@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,208 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
|
|
6
|
-
const authApi = vi.hoisted(() => ({
|
|
7
|
-
submitRegistrationRequest: vi.fn(),
|
|
8
|
-
}));
|
|
9
|
-
|
|
10
|
-
vi.mock('../src/auth/authApi', () => authApi);
|
|
11
|
-
vi.mock('react-router-dom', () => ({
|
|
12
|
-
useNavigate: () => vi.fn(),
|
|
13
|
-
useLocation: () => ({ search: '' }),
|
|
14
|
-
}));
|
|
15
|
-
vi.mock('react-helmet', () => ({ Helmet: ({ children }) => <>{children}</> }));
|
|
16
|
-
vi.mock('react-i18next', () => {
|
|
17
|
-
const t = (key, fallback) => (typeof fallback === 'string' ? fallback : key);
|
|
18
|
-
return {
|
|
19
|
-
useTranslation: () => ({
|
|
20
|
-
t,
|
|
21
|
-
// resolveErrorText (AUTH-10) reads i18n.exists/i18n.t off the object
|
|
22
|
-
// useTranslation() returns — this mock must carry both, not just `t`.
|
|
23
|
-
// No real catalogue here, so nothing "exists"; resolveErrorText falls
|
|
24
|
-
// through to the backend message or the caller's default key, exactly
|
|
25
|
-
// as it did before this mock had an i18n object at all.
|
|
26
|
-
i18n: { exists: () => false, t },
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
});
|
|
30
|
-
|
|
31
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
32
|
-
import { SignUpPage } from '../src/pages/SignUpPage';
|
|
33
|
-
|
|
34
|
-
function renderSignUp({ signup = true, signupModes, siteKey }) {
|
|
35
|
-
return render(
|
|
36
|
-
<AuthContext.Provider value={{ authMethods: {
|
|
37
|
-
signup,
|
|
38
|
-
signup_modes: signupModes,
|
|
39
|
-
turnstile_site_key: siteKey,
|
|
40
|
-
} }}>
|
|
41
|
-
<SignUpPage />
|
|
42
|
-
</AuthContext.Provider>,
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe('SignUpPage Turnstile integration', () => {
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
vi.clearAllMocks();
|
|
49
|
-
authApi.submitRegistrationRequest.mockResolvedValue({});
|
|
50
|
-
window.turnstile = {
|
|
51
|
-
render: vi.fn(() => 'widget-id'),
|
|
52
|
-
remove: vi.fn(),
|
|
53
|
-
reset: vi.fn(),
|
|
54
|
-
};
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
afterEach(() => {
|
|
58
|
-
cleanup();
|
|
59
|
-
delete window.turnstile;
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('renders only for open and email-domain signup modes when the site key is present', async () => {
|
|
63
|
-
renderSignUp({
|
|
64
|
-
signupModes: ['self_signup_open', 'self_signup_access_code', 'self_signup_email_domain'],
|
|
65
|
-
siteKey: 'site-key',
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
await waitFor(() => expect(screen.getByTestId('turnstile-widget')).toBeTruthy());
|
|
69
|
-
fireEvent.click(screen.getByRole('button', { name: 'self_signup_access_code' }));
|
|
70
|
-
expect(screen.queryByTestId('turnstile-widget')).toBeNull();
|
|
71
|
-
|
|
72
|
-
fireEvent.click(screen.getByRole('button', { name: 'self_signup_email_domain' }));
|
|
73
|
-
await waitFor(() => expect(screen.getByTestId('turnstile-widget')).toBeTruthy());
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('explains that signup is closed and omits the form when no signup modes are available', () => {
|
|
77
|
-
renderSignUp({ signup: false, signupModes: [] });
|
|
78
|
-
|
|
79
|
-
expect(screen.getByText('Self sign-up is currently disabled.')).toBeTruthy();
|
|
80
|
-
expect(screen.queryByRole('textbox', { name: /Auth\.EMAIL_LABEL/ })).toBeNull();
|
|
81
|
-
expect(screen.queryByRole('textbox', { name: /Auth\.ACCESS_CODE_LABEL/ })).toBeNull();
|
|
82
|
-
expect(screen.queryByRole('button', { name: 'Auth.SIGNUP_SUBMIT' })).toBeNull();
|
|
83
|
-
// A closed page must still leave a way forward, not just say why the
|
|
84
|
-
// form is missing (caught by review: the first implementation hid this
|
|
85
|
-
// too).
|
|
86
|
-
expect(screen.getByRole('button', { name: 'Auth.SIGNUP_GO_TO_LOGIN' })).toBeTruthy();
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('gates submission until verified and passes the Turnstile token to the registration API', async () => {
|
|
90
|
-
renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
|
|
91
|
-
|
|
92
|
-
await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
|
|
93
|
-
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
|
|
94
|
-
target: { value: 'ada@example.com' },
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
const submit = screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' });
|
|
98
|
-
expect(submit.disabled).toBe(true);
|
|
99
|
-
|
|
100
|
-
await act(async () => {
|
|
101
|
-
window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
await waitFor(() => expect(submit.disabled).toBe(false));
|
|
105
|
-
fireEvent.click(submit);
|
|
106
|
-
|
|
107
|
-
await waitFor(() => expect(authApi.submitRegistrationRequest).toHaveBeenCalledWith(
|
|
108
|
-
expect.objectContaining({
|
|
109
|
-
email: 'ada@example.com',
|
|
110
|
-
mode: 'self_signup_open',
|
|
111
|
-
turnstileToken: 'turnstile-token',
|
|
112
|
-
}),
|
|
113
|
-
));
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('resets the widget after a failed protected signup submission', async () => {
|
|
117
|
-
authApi.submitRegistrationRequest.mockRejectedValue({ code: 'Auth.INVITE_FAILED' });
|
|
118
|
-
renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
|
|
119
|
-
|
|
120
|
-
await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
|
|
121
|
-
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
|
|
122
|
-
target: { value: 'ada@example.com' },
|
|
123
|
-
});
|
|
124
|
-
await act(async () => {
|
|
125
|
-
window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
fireEvent.click(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }));
|
|
129
|
-
|
|
130
|
-
await waitFor(() => expect(window.turnstile.reset).toHaveBeenCalledWith('widget-id'));
|
|
131
|
-
expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(true);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('resets the widget and gates submission when its token expires', async () => {
|
|
135
|
-
renderSignUp({ signupModes: ['self_signup_open'], siteKey: 'site-key' });
|
|
136
|
-
|
|
137
|
-
await waitFor(() => expect(window.turnstile.render).toHaveBeenCalledOnce());
|
|
138
|
-
await act(async () => {
|
|
139
|
-
window.turnstile.render.mock.calls[0][1].callback('turnstile-token');
|
|
140
|
-
});
|
|
141
|
-
await waitFor(() => expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(false));
|
|
142
|
-
|
|
143
|
-
await act(async () => {
|
|
144
|
-
window.turnstile.render.mock.calls[0][1]['expired-callback']();
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
expect(window.turnstile.reset).toHaveBeenCalledWith('widget-id');
|
|
148
|
-
expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }).disabled).toBe(true);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it.each([
|
|
152
|
-
['self_signup_access_code', 'site-key'],
|
|
153
|
-
['self_signup_qr', 'site-key'],
|
|
154
|
-
['self_signup_open', ''],
|
|
155
|
-
])('does not render for %s without an eligible key/mode combination', async (mode, siteKey) => {
|
|
156
|
-
renderSignUp({ signupModes: [mode], siteKey });
|
|
157
|
-
|
|
158
|
-
await waitFor(() => expect(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' })).toBeTruthy());
|
|
159
|
-
expect(screen.queryByTestId('turnstile-widget')).toBeNull();
|
|
160
|
-
expect(window.turnstile.render).not.toHaveBeenCalled();
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('SignUpPage — success message reflects the backend response, not a hardcoded mode', () => {
|
|
165
|
-
beforeEach(() => {
|
|
166
|
-
vi.clearAllMocks();
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
afterEach(() => {
|
|
170
|
-
cleanup();
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
async function submit(mode) {
|
|
174
|
-
renderSignUp({ signupModes: [mode] });
|
|
175
|
-
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.EMAIL_LABEL/ }), {
|
|
176
|
-
target: { value: 'ada@example.com' },
|
|
177
|
-
});
|
|
178
|
-
if (mode === 'self_signup_access_code') {
|
|
179
|
-
fireEvent.change(screen.getByRole('textbox', { name: /Auth\.ACCESS_CODE_LABEL/ }), {
|
|
180
|
-
target: { value: 'some-code' },
|
|
181
|
-
});
|
|
182
|
-
}
|
|
183
|
-
fireEvent.click(screen.getByRole('button', { name: 'Auth.SIGNUP_SUBMIT' }));
|
|
184
|
-
await waitFor(() => expect(authApi.submitRegistrationRequest).toHaveBeenCalled());
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
it('shows the backend-provided code for open self-signup, not the access-code-specific message', async () => {
|
|
188
|
-
authApi.submitRegistrationRequest.mockResolvedValue({ code: 'Auth.INVITE_SENT', email: 'ada@example.com', mode: 'self_signup_open' });
|
|
189
|
-
await submit('self_signup_open');
|
|
190
|
-
|
|
191
|
-
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
192
|
-
expect(screen.queryByText('Auth.INVITE_REQUEST_SUCCESS')).toBeNull();
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
it('shows the backend-provided code for access-code self-signup too', async () => {
|
|
196
|
-
authApi.submitRegistrationRequest.mockResolvedValue({ code: 'Auth.INVITE_SENT', email: 'ada@example.com', mode: 'self_signup_access_code' });
|
|
197
|
-
await submit('self_signup_access_code');
|
|
198
|
-
|
|
199
|
-
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('falls back to Auth.INVITE_SENT if the backend response carries no code', async () => {
|
|
203
|
-
authApi.submitRegistrationRequest.mockResolvedValue({});
|
|
204
|
-
await submit('self_signup_open');
|
|
205
|
-
|
|
206
|
-
await waitFor(() => expect(screen.getByText('Auth.INVITE_SENT')).toBeTruthy());
|
|
207
|
-
});
|
|
208
|
-
});
|
package/tests/SoftChip.test.jsx
DELETED
|
@@ -1,105 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
4
|
-
import { ThemeProvider, alpha, darken } from '@mui/material/styles';
|
|
5
|
-
|
|
6
|
-
import { createAppTheme } from '../src/theme';
|
|
7
|
-
import SoftChip from '../src/components/SoftChip';
|
|
8
|
-
|
|
9
|
-
const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
10
|
-
const TONES = ['success', 'warning', 'error', 'info'];
|
|
11
|
-
|
|
12
|
-
function renderChip(props) {
|
|
13
|
-
return render(<ThemeProvider theme={theme}><SoftChip {...props} /></ThemeProvider>);
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function rgb(color) {
|
|
17
|
-
const probe = document.createElement('div');
|
|
18
|
-
probe.style.color = color;
|
|
19
|
-
document.body.appendChild(probe);
|
|
20
|
-
const resolved = window.getComputedStyle(probe).color;
|
|
21
|
-
probe.remove();
|
|
22
|
-
return resolved;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
afterEach(() => cleanup());
|
|
26
|
-
|
|
27
|
-
describe('SoftChip', () => {
|
|
28
|
-
it.each(TONES)('derives fill/border/text for tone "%s" from the theme, not a literal', (tone) => {
|
|
29
|
-
renderChip({ label: 'Provisional ranges', tone });
|
|
30
|
-
|
|
31
|
-
const chip = screen.getByText('Provisional ranges').closest('.MuiChip-root');
|
|
32
|
-
const style = window.getComputedStyle(chip);
|
|
33
|
-
const palette = theme.palette[tone];
|
|
34
|
-
|
|
35
|
-
expect(style.backgroundColor).toBe(rgb(alpha(palette.main, 0.12)));
|
|
36
|
-
expect(style.borderColor).toBe(rgb(alpha(palette.main, 0.3)));
|
|
37
|
-
expect(style.color).toBe(rgb(darken(palette.main, 0.3)));
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('differs between status and caveat in radius and typography', () => {
|
|
41
|
-
renderChip({ label: 'passed', tone: 'success', variant: 'status' });
|
|
42
|
-
const statusChip = screen.getByText('passed').closest('.MuiChip-root');
|
|
43
|
-
const statusLabel = window.getComputedStyle(screen.getByText('passed'));
|
|
44
|
-
expect(window.getComputedStyle(statusChip).borderRadius).toBe('999px');
|
|
45
|
-
// status uses the baseline's `overline` variant verbatim -- 11/600, uppercase, .4px tracking
|
|
46
|
-
expect(statusLabel.textTransform).toBe('uppercase');
|
|
47
|
-
expect(statusLabel.fontSize).toBe(theme.typography.overline.fontSize);
|
|
48
|
-
expect(statusLabel.fontWeight).toBe(String(theme.typography.overline.fontWeight));
|
|
49
|
-
expect(statusLabel.letterSpacing).toBe(theme.typography.overline.letterSpacing);
|
|
50
|
-
|
|
51
|
-
cleanup();
|
|
52
|
-
|
|
53
|
-
renderChip({ label: 'Provisional ranges', tone: 'warning', variant: 'caveat' });
|
|
54
|
-
const caveatChip = screen.getByText('Provisional ranges').closest('.MuiChip-root');
|
|
55
|
-
const caveatLabel = window.getComputedStyle(screen.getByText('Provisional ranges'));
|
|
56
|
-
expect(window.getComputedStyle(caveatChip).borderRadius).not.toBe('999px');
|
|
57
|
-
// caveat keeps its raw 12/500 -- the PRIM-1 token delta, deliberately not on a theme variant
|
|
58
|
-
expect(caveatLabel.fontSize).toBe('12px');
|
|
59
|
-
expect(caveatLabel.fontWeight).toBe('500');
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('defaults the leading element to a dot and swaps it for a passed icon', () => {
|
|
63
|
-
const { container: withDot } = renderChip({ label: 'passed', tone: 'success', variant: 'status' });
|
|
64
|
-
expect(withDot.querySelector('.MuiChip-icon')).toBeTruthy();
|
|
65
|
-
|
|
66
|
-
cleanup();
|
|
67
|
-
|
|
68
|
-
const { container: withIcon } = renderChip({
|
|
69
|
-
label: 'recurring',
|
|
70
|
-
tone: 'info',
|
|
71
|
-
variant: 'status',
|
|
72
|
-
icon: <span data-testid="custom-icon" />,
|
|
73
|
-
});
|
|
74
|
-
expect(withIcon.querySelector('[data-testid="custom-icon"]')).toBeTruthy();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('toggles the border', () => {
|
|
78
|
-
renderChip({ label: 'failed', tone: 'error', variant: 'status', border: false });
|
|
79
|
-
|
|
80
|
-
const chip = screen.getByText('failed').closest('.MuiChip-root');
|
|
81
|
-
expect(window.getComputedStyle(chip).borderColor).toBe(rgb('transparent'));
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('renders title as a tooltip, reachable on a touch pointer with no delay', async () => {
|
|
85
|
-
renderChip({ label: 'queued', tone: 'info', variant: 'status', title: 'Waiting on the sandbox' });
|
|
86
|
-
|
|
87
|
-
expect(screen.queryByText('Waiting on the sandbox')).toBeNull();
|
|
88
|
-
|
|
89
|
-
const chip = screen.getByText('queued').closest('.MuiChip-root');
|
|
90
|
-
fireEvent.touchStart(chip);
|
|
91
|
-
|
|
92
|
-
// `enterTouchDelay={0}` is the fix for the AUTH-4 failure mode -- a tooltip a touch pointer
|
|
93
|
-
// can never trigger. waitFor's default timeout (1000ms) is generous headroom around "no
|
|
94
|
-
// delay"; a regression to the ~700ms default would still likely pass, so this is a floor, not
|
|
95
|
-
// a precise timing assertion -- the source is the actual guarantee, this proves it's wired.
|
|
96
|
-
await waitFor(() => expect(screen.getByText('Waiting on the sandbox')).toBeTruthy());
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('omits the tooltip wrapper entirely when there is no title', () => {
|
|
100
|
-
renderChip({ label: 'queued', tone: 'info', variant: 'status' });
|
|
101
|
-
|
|
102
|
-
const chip = screen.getByText('queued').closest('.MuiChip-root');
|
|
103
|
-
expect(chip.getAttribute('aria-label')).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
});
|
package/tests/StatTile.test.jsx
DELETED
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import { afterEach, describe, expect, it } from 'vitest';
|
|
3
|
-
import { cleanup, render, screen } from '@testing-library/react';
|
|
4
|
-
import { ThemeProvider } from '@mui/material/styles';
|
|
5
|
-
|
|
6
|
-
import { createAppTheme } from '../src/theme';
|
|
7
|
-
import StatTile from '../src/components/StatTile';
|
|
8
|
-
|
|
9
|
-
const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
10
|
-
|
|
11
|
-
function renderTile(props) {
|
|
12
|
-
return render(<ThemeProvider theme={theme}><StatTile {...props} /></ThemeProvider>);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
afterEach(() => cleanup());
|
|
16
|
-
|
|
17
|
-
describe('StatTile', () => {
|
|
18
|
-
it('renders label, value and caption', () => {
|
|
19
|
-
renderTile({ label: 'Coverage at budget', value: '71.4 %', caption: 'of maximum realizable' });
|
|
20
|
-
|
|
21
|
-
expect(screen.getByText('Coverage at budget')).toBeTruthy();
|
|
22
|
-
expect(screen.getByText('71.4 %')).toBeTruthy();
|
|
23
|
-
expect(screen.getByText('of maximum realizable')).toBeTruthy();
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
it('omits the value row when value is undefined', () => {
|
|
27
|
-
renderTile({ label: 'Recovered' });
|
|
28
|
-
|
|
29
|
-
expect(screen.getByText('Recovered')).toBeTruthy();
|
|
30
|
-
expect(screen.queryByText('undefined')).toBeNull();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('renders children', () => {
|
|
34
|
-
renderTile({ label: 'Deaths averted', value: '63.8 %', children: <span>children slot</span> });
|
|
35
|
-
|
|
36
|
-
expect(screen.getByText('children slot')).toBeTruthy();
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('applies the accent border only when accent is set', () => {
|
|
40
|
-
const { container: plain } = renderTile({ label: 'Body fat', value: '18.4 %' });
|
|
41
|
-
const plainStyle = window.getComputedStyle(plain.firstChild);
|
|
42
|
-
expect(plainStyle.borderWidth).toBe('1px');
|
|
43
|
-
|
|
44
|
-
cleanup();
|
|
45
|
-
|
|
46
|
-
const { container: accented } = renderTile({ label: 'Lean mass', value: '62.1 kg', accent: true });
|
|
47
|
-
const accentedStyle = window.getComputedStyle(accented.firstChild);
|
|
48
|
-
expect(accentedStyle.borderWidth).toBe('2px');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('renders the value with tabular-nums', () => {
|
|
52
|
-
renderTile({ label: 'theta*', value: '0.8421' });
|
|
53
|
-
|
|
54
|
-
const value = screen.getByText('0.8421');
|
|
55
|
-
expect(window.getComputedStyle(value).fontVariantNumeric).toBe('tabular-nums');
|
|
56
|
-
});
|
|
57
|
-
});
|
package/tests/Thread.test.jsx
DELETED
|
@@ -1,151 +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
|
-
vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key, options) => options?.count != null ? `${key}:${options.count}` : key }) }));
|
|
7
|
-
vi.mock('../src/notifications/realtime', () => ({ useRealtime: () => ({ subscribe: () => () => {}, onReconnect: () => () => {} }) }));
|
|
8
|
-
|
|
9
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
10
|
-
import { MessageBubble } from '../src/messaging/MessageBubble';
|
|
11
|
-
import { MessagingProvider } from '../src/messaging/MessagingProvider';
|
|
12
|
-
import { ReadTicks } from '../src/messaging/ReadTicks';
|
|
13
|
-
import { Thread } from '../src/messaging/Thread';
|
|
14
|
-
|
|
15
|
-
function makeApi() {
|
|
16
|
-
return {
|
|
17
|
-
listConversations: vi.fn().mockResolvedValue({ results: [{ id: 1, kind: 'group' }] }),
|
|
18
|
-
getUnreadCount: vi.fn().mockResolvedValue({ unread_count: 0, by_conversation: {} }),
|
|
19
|
-
listMessages: vi.fn().mockResolvedValue({ results: [{ id: 2, conversation_id: 1, body: 'Visible message', created_at: '2026-07-31T10:00:00Z' }], next_cursor: 'signed-opaque-cursor' }),
|
|
20
|
-
listThread: vi.fn().mockResolvedValue({ results: [] }),
|
|
21
|
-
getReadStatus: vi.fn().mockResolvedValue({ all_read: false }),
|
|
22
|
-
};
|
|
23
|
-
}
|
|
24
|
-
// No `activeConversationId` on the provider: this matches how jg-ferien
|
|
25
|
-
// actually mounts it (the provider sits above the page that tracks which
|
|
26
|
-
// conversation is open, with no way to keep that prop in sync) — `Thread`'s
|
|
27
|
-
// own conversationId-driven fetch is the one exercised here, not the
|
|
28
|
-
// provider's separate activeConversationId-driven mount fetch (that path has
|
|
29
|
-
// its own dedicated coverage in MessagingProvider.test.jsx).
|
|
30
|
-
function renderThread(api, { userId = 9 } = {}) {
|
|
31
|
-
return render(
|
|
32
|
-
<AuthContext.Provider value={{ user: { id: userId } }}>
|
|
33
|
-
<MessagingProvider api={api}><Thread conversationId={1} /></MessagingProvider>
|
|
34
|
-
</AuthContext.Provider>,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe('messaging Thread surfaces', () => {
|
|
39
|
-
let api;
|
|
40
|
-
beforeEach(() => { api = makeApi(); });
|
|
41
|
-
afterEach(cleanup);
|
|
42
|
-
|
|
43
|
-
it('uses the opaque message cursor and shows a rejected older page as an error', async () => {
|
|
44
|
-
api.listMessages.mockResolvedValueOnce({ results: [{ id: 2, conversation_id: 1, body: 'Visible message' }], next_cursor: 'signed-opaque-cursor' }).mockRejectedValueOnce(new Error('bad cursor'));
|
|
45
|
-
renderThread(api);
|
|
46
|
-
await screen.findByText('Visible message');
|
|
47
|
-
fireEvent.click(screen.getByRole('button', { name: 'MessagingThread.LOAD_OLDER' }));
|
|
48
|
-
await screen.findByRole('alert');
|
|
49
|
-
expect(api.listMessages).toHaveBeenLastCalledWith(1, { cursor: 'signed-opaque-cursor' });
|
|
50
|
-
expect(screen.getByText('Visible message')).toBeTruthy();
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('renders deleted content as a redacted placeholder', () => {
|
|
54
|
-
render(<MessageBubble message={{ id: 4, body: 'Stale secret', deleted_at: '2026-07-31T10:00:00Z' }} />);
|
|
55
|
-
expect(screen.getByText('MessagingThread.DELETED')).toBeTruthy();
|
|
56
|
-
expect(screen.queryByText('Stale secret')).toBeNull();
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
it('renders the two-state direct tick and never exposes supplied recipient details for a direct conversation', async () => {
|
|
60
|
-
api.getReadStatus.mockResolvedValue({ all_read: false, recipient_detail: [{ display_name: 'Private recipient' }] });
|
|
61
|
-
render(<MessagingProvider api={api} active={false}><ReadTicks messageId={2} conversation={{ id: 1, kind: 'direct' }} /></MessagingProvider>);
|
|
62
|
-
await screen.findByLabelText('MessagingReadTicks.SENT');
|
|
63
|
-
expect(screen.queryByText('Private recipient')).toBeNull();
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('renders supplied recipient detail for a non-direct conversation with counts (the permitted case)', async () => {
|
|
67
|
-
api.getReadStatus.mockResolvedValue({ all_read: false, read_count: 1, recipient_count: 2, recipient_detail: [{ display_name: 'Visible recipient' }] });
|
|
68
|
-
render(<MessagingProvider api={api} active={false}><ReadTicks messageId={2} conversation={{ id: 1, kind: 'group' }} /></MessagingProvider>);
|
|
69
|
-
const tick = await screen.findByLabelText('MessagingReadTicks.READ_RATIO');
|
|
70
|
-
fireEvent.click(tick);
|
|
71
|
-
await screen.findByText('Visible recipient');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('only mounts ReadTicks (and its REST call) for the current user\'s own messages, not every message in the timeline', async () => {
|
|
75
|
-
api.listMessages.mockResolvedValue({
|
|
76
|
-
results: [
|
|
77
|
-
{ id: 2, conversation_id: 1, body: 'Mine', created_at: '2026-07-31T10:00:00Z', sender: { id: 9 } },
|
|
78
|
-
{ id: 3, conversation_id: 1, body: 'Theirs', created_at: '2026-07-31T10:01:00Z', sender: { id: 42 } },
|
|
79
|
-
],
|
|
80
|
-
next_cursor: null,
|
|
81
|
-
});
|
|
82
|
-
renderThread(api, { userId: 9 });
|
|
83
|
-
await screen.findByText('Mine');
|
|
84
|
-
await screen.findByText('Theirs');
|
|
85
|
-
await waitFor(() => expect(api.getReadStatus).toHaveBeenCalledTimes(1));
|
|
86
|
-
expect(api.getReadStatus).toHaveBeenCalledWith(2);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('does not mount ReadTicks (or fire a REST call) for a still-pending optimistic own message', async () => {
|
|
90
|
-
api.listMessages.mockResolvedValue({
|
|
91
|
-
results: [{ id: 'local-r9', conversation_id: 1, body: 'Sending...', created_at: '2026-07-31T10:00:00Z', sender: { id: 9 }, status: 'pending', client_request_id: 'r9' }],
|
|
92
|
-
next_cursor: null,
|
|
93
|
-
});
|
|
94
|
-
renderThread(api, { userId: 9 });
|
|
95
|
-
await screen.findByText('Sending...');
|
|
96
|
-
expect(api.getReadStatus).not.toHaveBeenCalled();
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('mounts Thread and ReadTicks independently', async () => {
|
|
100
|
-
renderThread(api);
|
|
101
|
-
await screen.findByLabelText('MessagingThread.TIMELINE');
|
|
102
|
-
cleanup();
|
|
103
|
-
api.getReadStatus.mockResolvedValue({ all_read: false, read_count: 0, recipient_count: 3 });
|
|
104
|
-
render(<MessagingProvider api={api} active={false}><ReadTicks messageId={2} conversation={{ id: 1, kind: 'group' }} /></MessagingProvider>);
|
|
105
|
-
await waitFor(() => expect(screen.getByLabelText('MessagingReadTicks.READ_RATIO')).toBeTruthy());
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('fetches and shows a conversation\'s historical messages on open, with no realtime frame or optimistic send involved', async () => {
|
|
109
|
-
// The bug this pins: nothing else in Thread/MessagingProvider ever calls
|
|
110
|
-
// listMessages for the conversation actually opened when the host (like
|
|
111
|
-
// jg-ferien) never wires `activeConversationId` into the provider itself
|
|
112
|
-
// — cache.messages was otherwise only ever populated by a live frame or
|
|
113
|
-
// this browser's own optimistic send, so any conversation whose only
|
|
114
|
-
// messages predate the current session rendered as permanently empty
|
|
115
|
-
// despite the conversation list correctly showing a last-message preview.
|
|
116
|
-
renderThread(api);
|
|
117
|
-
expect(await screen.findByText('Visible message')).toBeTruthy();
|
|
118
|
-
expect(screen.queryByText('MessagingThread.EMPTY')).toBeNull();
|
|
119
|
-
expect(api.listMessages).toHaveBeenCalledWith(1);
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('does not flash the empty state while the initial fetch is still pending', async () => {
|
|
123
|
-
let resolveMessages;
|
|
124
|
-
api.listMessages.mockReturnValueOnce(new Promise((resolve) => { resolveMessages = resolve; }));
|
|
125
|
-
renderThread(api);
|
|
126
|
-
expect(screen.queryByText('MessagingThread.EMPTY')).toBeNull();
|
|
127
|
-
resolveMessages({ results: [{ id: 2, conversation_id: 1, body: 'Visible message' }], next_cursor: null });
|
|
128
|
-
await screen.findByText('Visible message');
|
|
129
|
-
});
|
|
130
|
-
|
|
131
|
-
it('does not cover already-cached messages with the initial-load spinner while a redundant re-fetch is in flight', async () => {
|
|
132
|
-
function Switcher({ conversationId }) {
|
|
133
|
-
return <AuthContext.Provider value={{ user: { id: 9 } }}><MessagingProvider api={api}><Thread conversationId={conversationId} /></MessagingProvider></AuthContext.Provider>;
|
|
134
|
-
}
|
|
135
|
-
// First open (conversation 1): messages load and cache normally.
|
|
136
|
-
const { rerender } = render(<Switcher conversationId={1} />);
|
|
137
|
-
await screen.findByText('Visible message');
|
|
138
|
-
// Switch away, then back to conversation 1 — the effect re-fires (a real
|
|
139
|
-
// "close and reopen" or focus-regain), triggering a redundant re-fetch
|
|
140
|
-
// against a cache that already has this conversation's messages. Make
|
|
141
|
-
// that second fetch (the one for the switch BACK to 1) hang so the
|
|
142
|
-
// assertion below is meaningful.
|
|
143
|
-
rerender(<Switcher conversationId={2} />);
|
|
144
|
-
let resolveSecondFetch;
|
|
145
|
-
api.listMessages.mockImplementationOnce(() => new Promise((resolve) => { resolveSecondFetch = resolve; }));
|
|
146
|
-
rerender(<Switcher conversationId={1} />);
|
|
147
|
-
expect(screen.getByText('Visible message')).toBeTruthy();
|
|
148
|
-
resolveSecondFetch({ results: [{ id: 2, conversation_id: 1, body: 'Visible message' }], next_cursor: null });
|
|
149
|
-
await waitFor(() => expect(screen.getByText('Visible message')).toBeTruthy());
|
|
150
|
-
});
|
|
151
|
-
});
|