@micha.bigler/ui-core-micha 3.14.4 → 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 +25 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +32 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
|
@@ -1,276 +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 notificationsApi = vi.hoisted(() => ({
|
|
7
|
-
getNotificationPreferences: vi.fn(),
|
|
8
|
-
getVapidPublicKey: vi.fn(),
|
|
9
|
-
patchNotificationPreferences: vi.fn(),
|
|
10
|
-
removePushSubscription: vi.fn(),
|
|
11
|
-
savePushSubscription: vi.fn(),
|
|
12
|
-
setNotificationCategorySubscription: vi.fn(),
|
|
13
|
-
urlBase64ToUint8Array: vi.fn(() => new Uint8Array([1, 2, 3])),
|
|
14
|
-
}));
|
|
15
|
-
|
|
16
|
-
vi.mock('../src/notifications/api', () => notificationsApi);
|
|
17
|
-
// A stable `t` reference, matching real react-i18next (which memoizes it) -- an unstable
|
|
18
|
-
// mock `t` would re-trigger the component's `useEffect(..., [t])` on every render and
|
|
19
|
-
// re-fetch preferences, clobbering any state update made in between (e.g. a toggle's
|
|
20
|
-
// optimistic-from-response update racing against a stale refetch).
|
|
21
|
-
const stableT = (key) => key;
|
|
22
|
-
vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: stableT }) }));
|
|
23
|
-
|
|
24
|
-
import { NotificationSettings } from '../src/notifications/NotificationSettings';
|
|
25
|
-
|
|
26
|
-
function installPushEnvironment(subscription) {
|
|
27
|
-
const registration = {
|
|
28
|
-
pushManager: {
|
|
29
|
-
getSubscription: vi.fn().mockResolvedValue(subscription),
|
|
30
|
-
subscribe: vi.fn(),
|
|
31
|
-
},
|
|
32
|
-
};
|
|
33
|
-
vi.stubGlobal('navigator', {
|
|
34
|
-
serviceWorker: { ready: Promise.resolve(registration) },
|
|
35
|
-
userAgent: 'Unit Test Browser',
|
|
36
|
-
});
|
|
37
|
-
Object.defineProperty(window, 'PushManager', { value: class PushManager {}, configurable: true });
|
|
38
|
-
return registration;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
afterEach(() => {
|
|
42
|
-
cleanup();
|
|
43
|
-
vi.unstubAllGlobals();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
describe('NotificationSettings push toggle', () => {
|
|
47
|
-
beforeEach(() => {
|
|
48
|
-
vi.clearAllMocks();
|
|
49
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: false, push_opt_in: false });
|
|
50
|
-
notificationsApi.patchNotificationPreferences.mockResolvedValue({ email_opt_in: false, push_opt_in: true });
|
|
51
|
-
notificationsApi.getVapidPublicKey.mockResolvedValue('AQID');
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('enables push on this device and opts in after saving the subscription', async () => {
|
|
55
|
-
const registration = installPushEnvironment(null);
|
|
56
|
-
const subscription = {
|
|
57
|
-
toJSON: vi.fn(() => ({ endpoint: 'https://fcm.googleapis.com/fcm/send/example' })),
|
|
58
|
-
};
|
|
59
|
-
registration.pushManager.subscribe.mockResolvedValue(subscription);
|
|
60
|
-
vi.stubGlobal('Notification', { requestPermission: vi.fn().mockResolvedValue('granted') });
|
|
61
|
-
|
|
62
|
-
render(<NotificationSettings />);
|
|
63
|
-
fireEvent.click(await screen.findByRole('switch', { name: /PUSH_LABEL/ }));
|
|
64
|
-
|
|
65
|
-
await waitFor(() => expect(notificationsApi.patchNotificationPreferences).toHaveBeenCalledWith({ push_opt_in: true }));
|
|
66
|
-
expect(Notification.requestPermission).toHaveBeenCalledOnce();
|
|
67
|
-
expect(registration.pushManager.subscribe).toHaveBeenCalledWith({
|
|
68
|
-
userVisibleOnly: true,
|
|
69
|
-
applicationServerKey: new Uint8Array([1, 2, 3]),
|
|
70
|
-
});
|
|
71
|
-
expect(notificationsApi.savePushSubscription).toHaveBeenCalledWith(
|
|
72
|
-
{ endpoint: 'https://fcm.googleapis.com/fcm/send/example' },
|
|
73
|
-
'Unit Test Browser',
|
|
74
|
-
);
|
|
75
|
-
expect(notificationsApi.savePushSubscription.mock.invocationCallOrder[0])
|
|
76
|
-
.toBeLessThan(notificationsApi.patchNotificationPreferences.mock.invocationCallOrder[0]);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('unsubscribes and removes only this device without changing push_opt_in', async () => {
|
|
80
|
-
const subscription = {
|
|
81
|
-
endpoint: 'https://updates.push.services.mozilla.com/example',
|
|
82
|
-
unsubscribe: vi.fn().mockResolvedValue(true),
|
|
83
|
-
};
|
|
84
|
-
installPushEnvironment(subscription);
|
|
85
|
-
|
|
86
|
-
render(<NotificationSettings />);
|
|
87
|
-
fireEvent.click(await screen.findByRole('switch', { name: /PUSH_LABEL/ }));
|
|
88
|
-
|
|
89
|
-
await waitFor(() => expect(subscription.unsubscribe).toHaveBeenCalledOnce());
|
|
90
|
-
expect(notificationsApi.removePushSubscription).toHaveBeenCalledWith({ endpoint: subscription.endpoint });
|
|
91
|
-
expect(notificationsApi.patchNotificationPreferences).not.toHaveBeenCalled();
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('NotificationSettings push preview toggle', () => {
|
|
96
|
-
beforeEach(() => {
|
|
97
|
-
vi.clearAllMocks();
|
|
98
|
-
notificationsApi.getVapidPublicKey.mockResolvedValue('AQID');
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('renders checked when the preference is explicitly on', async () => {
|
|
102
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
103
|
-
email_opt_in: false, push_opt_in: true, push_preview_opt_in: true,
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
render(<NotificationSettings />);
|
|
107
|
-
|
|
108
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
109
|
-
expect(toggle.checked).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('renders checked when the preference is absent entirely (default-on, not Boolean(undefined))', async () => {
|
|
113
|
-
// Regression test for NOTIF-14: a naive `Boolean(preferences?.push_preview_opt_in)`
|
|
114
|
-
// reads a missing field as false, misrepresenting the backend's true default. Must
|
|
115
|
-
// fail against that implementation.
|
|
116
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({ email_opt_in: false, push_opt_in: true });
|
|
117
|
-
|
|
118
|
-
render(<NotificationSettings />);
|
|
119
|
-
|
|
120
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
121
|
-
expect(toggle.checked).toBe(true);
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('renders unchecked when the preference is explicitly off', async () => {
|
|
125
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
126
|
-
email_opt_in: false, push_opt_in: true, push_preview_opt_in: false,
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
render(<NotificationSettings />);
|
|
130
|
-
|
|
131
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
132
|
-
expect(toggle.checked).toBe(false);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('toggling calls patchNotificationPreferences with push_preview_opt_in and re-renders from the response', async () => {
|
|
136
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
137
|
-
email_opt_in: false, push_opt_in: true, push_preview_opt_in: true,
|
|
138
|
-
});
|
|
139
|
-
notificationsApi.patchNotificationPreferences.mockResolvedValue({
|
|
140
|
-
email_opt_in: false, push_opt_in: true, push_preview_opt_in: false,
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
render(<NotificationSettings />);
|
|
144
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
145
|
-
fireEvent.click(toggle);
|
|
146
|
-
|
|
147
|
-
await waitFor(() => expect(notificationsApi.patchNotificationPreferences).toHaveBeenCalledWith({ push_preview_opt_in: false }));
|
|
148
|
-
await waitFor(() => expect(toggle.checked).toBe(false));
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('is disabled when push notifications are not opted in for this account', async () => {
|
|
152
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
153
|
-
email_opt_in: false, push_opt_in: false, push_preview_opt_in: true,
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
render(<NotificationSettings />);
|
|
157
|
-
|
|
158
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
159
|
-
expect(toggle.disabled).toBe(true);
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('is enabled when push notifications are opted in for this account', async () => {
|
|
163
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
164
|
-
email_opt_in: false, push_opt_in: true, push_preview_opt_in: true,
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
render(<NotificationSettings />);
|
|
168
|
-
|
|
169
|
-
const toggle = await screen.findByRole('switch', { name: /PUSH_PREVIEW_LABEL/ });
|
|
170
|
-
expect(toggle.disabled).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('NotificationSettings reach axis (NOTIF-26)', () => {
|
|
175
|
-
beforeEach(() => {
|
|
176
|
-
vi.clearAllMocks();
|
|
177
|
-
notificationsApi.getVapidPublicKey.mockResolvedValue('AQID');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('renders no reach or subscriptions sections when the server reports none (no new props)', async () => {
|
|
181
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
182
|
-
email_opt_in: false, push_opt_in: false, notification_types: [], subscribable_categories: [],
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
render(<NotificationSettings />);
|
|
186
|
-
await screen.findByRole('switch', { name: /EMAIL_LABEL/ });
|
|
187
|
-
|
|
188
|
-
expect(screen.queryByText('NotificationSettings.REACH_TITLE')).toBeNull();
|
|
189
|
-
expect(screen.queryByText('NotificationSettings.SUBSCRIPTIONS_TITLE')).toBeNull();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('shows the both-reach message for a type that is active and passive', async () => {
|
|
193
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
194
|
-
email_opt_in: true, push_opt_in: false,
|
|
195
|
-
notification_types: [{ key: 'status.deploy_failed', category: 'status', label: 'Deploy failed', active: true, passive: true, has_active_channel: true }],
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
render(<NotificationSettings />);
|
|
199
|
-
|
|
200
|
-
expect(await screen.findByText('Deploy failed')).toBeTruthy();
|
|
201
|
-
expect(await screen.findByText('NotificationSettings.REACH_BOTH')).toBeTruthy();
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it('shows the passive-only message for a type with no active reach', async () => {
|
|
205
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
206
|
-
email_opt_in: false, push_opt_in: false,
|
|
207
|
-
notification_types: [{ key: 'status.deploy_recovered', category: 'status', label: 'Deploy recovered', active: false, passive: true, has_active_channel: null }],
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
render(<NotificationSettings />);
|
|
211
|
-
|
|
212
|
-
expect(await screen.findByText('NotificationSettings.REACH_PASSIVE')).toBeTruthy();
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it('shows the no-active-channel warning for an active-only type the user cannot currently be reached on (scope C)', async () => {
|
|
216
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
217
|
-
email_opt_in: false, push_opt_in: false,
|
|
218
|
-
notification_types: [{ key: 'app.messaging.new_message', category: 'messaging', label: 'New messages', active: true, passive: false, has_active_channel: false }],
|
|
219
|
-
});
|
|
220
|
-
|
|
221
|
-
render(<NotificationSettings />);
|
|
222
|
-
|
|
223
|
-
expect(await screen.findByText(/NotificationSettings\.REACH_NO_ACTIVE_CHANNEL/)).toBeTruthy();
|
|
224
|
-
expect(await screen.findByText(/New messages/)).toBeTruthy();
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('shows the passive fallback (not the undelivered warning) for a both-type with no active channel configured', async () => {
|
|
228
|
-
// Regression: scope C's bounded fallback means a both-type still reaches the
|
|
229
|
-
// user passively (chip) with no active channel -- it must not be shown as
|
|
230
|
-
// undelivered, which is reserved for active-only types.
|
|
231
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
232
|
-
email_opt_in: false, push_opt_in: false,
|
|
233
|
-
notification_types: [{ key: 'status.deploy_failed', category: 'status', label: 'Deploy failed', active: true, passive: true, has_active_channel: false }],
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
render(<NotificationSettings />);
|
|
237
|
-
|
|
238
|
-
expect(await screen.findByText('NotificationSettings.REACH_PASSIVE')).toBeTruthy();
|
|
239
|
-
expect(screen.queryByText(/NotificationSettings\.REACH_NO_ACTIVE_CHANNEL/)).toBeNull();
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
it('renders server-reported subscribable categories and toggles a subscription', async () => {
|
|
243
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
244
|
-
email_opt_in: false, push_opt_in: false,
|
|
245
|
-
subscribable_categories: [{ category: 'ops', label: 'Operations', subscribed: false }],
|
|
246
|
-
});
|
|
247
|
-
notificationsApi.setNotificationCategorySubscription.mockResolvedValue({ category: 'ops', subscribed: true });
|
|
248
|
-
|
|
249
|
-
render(<NotificationSettings />);
|
|
250
|
-
const toggle = await screen.findByRole('switch', { name: 'Operations' });
|
|
251
|
-
expect(toggle.checked).toBe(false);
|
|
252
|
-
|
|
253
|
-
fireEvent.click(toggle);
|
|
254
|
-
|
|
255
|
-
await waitFor(() => expect(notificationsApi.setNotificationCategorySubscription).toHaveBeenCalledWith('ops', true));
|
|
256
|
-
await waitFor(() => expect(toggle.checked).toBe(true));
|
|
257
|
-
});
|
|
258
|
-
|
|
259
|
-
it('reflects the server response, not the clicked checkbox value, when they disagree', async () => {
|
|
260
|
-
// Regression: a 2xx response reporting a different `subscribed` (e.g. a
|
|
261
|
-
// server-side clamp) must win over what the user clicked.
|
|
262
|
-
notificationsApi.getNotificationPreferences.mockResolvedValue({
|
|
263
|
-
email_opt_in: false, push_opt_in: false,
|
|
264
|
-
subscribable_categories: [{ category: 'ops', label: 'Operations', subscribed: false }],
|
|
265
|
-
});
|
|
266
|
-
notificationsApi.setNotificationCategorySubscription.mockResolvedValue({ category: 'ops', subscribed: false });
|
|
267
|
-
|
|
268
|
-
render(<NotificationSettings />);
|
|
269
|
-
const toggle = await screen.findByRole('switch', { name: 'Operations' });
|
|
270
|
-
|
|
271
|
-
fireEvent.click(toggle);
|
|
272
|
-
|
|
273
|
-
await waitFor(() => expect(notificationsApi.setNotificationCategorySubscription).toHaveBeenCalledWith('ops', true));
|
|
274
|
-
await waitFor(() => expect(toggle.checked).toBe(false));
|
|
275
|
-
});
|
|
276
|
-
});
|
|
@@ -1,240 +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 feedApi = vi.hoisted(() => ({
|
|
7
|
-
getNotificationFeed: vi.fn(),
|
|
8
|
-
getUnreadCount: vi.fn(),
|
|
9
|
-
markNotifications: vi.fn(),
|
|
10
|
-
}));
|
|
11
|
-
|
|
12
|
-
vi.mock('../src/notifications/feedApi', () => feedApi);
|
|
13
|
-
|
|
14
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
15
|
-
import { NotificationsProvider, useNotifications } from '../src/notifications/NotificationsProvider';
|
|
16
|
-
|
|
17
|
-
class MockWebSocket {
|
|
18
|
-
static instances = [];
|
|
19
|
-
|
|
20
|
-
constructor(url) {
|
|
21
|
-
this.url = url;
|
|
22
|
-
this.close = vi.fn(() => this.onclose?.());
|
|
23
|
-
MockWebSocket.instances.push(this);
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const firstNotification = {
|
|
28
|
-
id: 1,
|
|
29
|
-
notification_id: 101,
|
|
30
|
-
content: { title_key: 'first.title' },
|
|
31
|
-
seen_at: null,
|
|
32
|
-
dismissed_at: null,
|
|
33
|
-
done_at: null,
|
|
34
|
-
};
|
|
35
|
-
|
|
36
|
-
const secondNotification = {
|
|
37
|
-
id: 2,
|
|
38
|
-
notification_id: 102,
|
|
39
|
-
content: { title_key: 'second.title' },
|
|
40
|
-
seen_at: null,
|
|
41
|
-
dismissed_at: null,
|
|
42
|
-
done_at: null,
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function Consumer() {
|
|
46
|
-
const notifications = useNotifications();
|
|
47
|
-
return (
|
|
48
|
-
<>
|
|
49
|
-
<output data-testid="notifications">{JSON.stringify(notifications.notifications)}</output>
|
|
50
|
-
<output data-testid="unread-count">{notifications.unreadCount}</output>
|
|
51
|
-
<button onClick={() => notifications.markSeen([1])}>seen</button>
|
|
52
|
-
<button onClick={() => notifications.markDismissed([2])}>dismissed</button>
|
|
53
|
-
<button onClick={() => notifications.markDone([1])}>done</button>
|
|
54
|
-
</>
|
|
55
|
-
);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
function renderProvider({ children = <Consumer />, user = { id: 4 }, ...providerProps } = {}) {
|
|
59
|
-
return render(
|
|
60
|
-
<AuthContext.Provider value={{ user }}>
|
|
61
|
-
<NotificationsProvider {...providerProps}>{children}</NotificationsProvider>
|
|
62
|
-
</AuthContext.Provider>,
|
|
63
|
-
);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function currentNotifications() {
|
|
67
|
-
return JSON.parse(screen.getByTestId('notifications').textContent);
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
describe('NotificationsProvider', () => {
|
|
71
|
-
afterEach(() => {
|
|
72
|
-
cleanup();
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
beforeEach(() => {
|
|
76
|
-
vi.clearAllMocks();
|
|
77
|
-
MockWebSocket.instances = [];
|
|
78
|
-
vi.stubGlobal('WebSocket', MockWebSocket);
|
|
79
|
-
feedApi.getNotificationFeed.mockResolvedValue({ results: [firstNotification, secondNotification] });
|
|
80
|
-
feedApi.getUnreadCount.mockResolvedValue({ count: 2 });
|
|
81
|
-
feedApi.markNotifications.mockResolvedValue({ updated: 1 });
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('seeds state from the feed and unread-count endpoints', async () => {
|
|
85
|
-
renderProvider();
|
|
86
|
-
|
|
87
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('2'));
|
|
88
|
-
|
|
89
|
-
expect(currentNotifications()).toEqual(expect.arrayContaining([
|
|
90
|
-
expect.objectContaining({ id: 1, notification_id: 101, seen: false }),
|
|
91
|
-
expect.objectContaining({ id: 2, notification_id: 102, dismissed: false }),
|
|
92
|
-
]));
|
|
93
|
-
// `seeded` (which gates the socket connect) flips in refresh()'s .finally(),
|
|
94
|
-
// one tick after unreadCount is set -- so the socket can still be unopened
|
|
95
|
-
// right when the unread-count waitFor above resolves. Wait for it directly
|
|
96
|
-
// instead of asserting synchronously.
|
|
97
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('prepends a new notification push and increments the unread count', async () => {
|
|
101
|
-
renderProvider();
|
|
102
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
103
|
-
|
|
104
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
105
|
-
type: 'payment_due',
|
|
106
|
-
notification_id: 303,
|
|
107
|
-
content: { title_key: 'payment.title', body_key: 'payment.body', params: { amount: 20 } },
|
|
108
|
-
}) });
|
|
109
|
-
|
|
110
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('3'));
|
|
111
|
-
expect(currentNotifications()[0]).toEqual(expect.objectContaining({
|
|
112
|
-
id: 303,
|
|
113
|
-
notification_id: 303,
|
|
114
|
-
notification_type: 'payment_due',
|
|
115
|
-
seen: false,
|
|
116
|
-
}));
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('applies a status push to the matching notification without duplication', async () => {
|
|
120
|
-
renderProvider();
|
|
121
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
122
|
-
|
|
123
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
124
|
-
type: 'notification.status',
|
|
125
|
-
notification_id: 101,
|
|
126
|
-
status: { seen: true, dismissed: false, done: true },
|
|
127
|
-
}) });
|
|
128
|
-
|
|
129
|
-
await waitFor(() => expect(currentNotifications()[0]).toEqual(expect.objectContaining({
|
|
130
|
-
notification_id: 101,
|
|
131
|
-
seen: true,
|
|
132
|
-
dismissed: false,
|
|
133
|
-
done: true,
|
|
134
|
-
})));
|
|
135
|
-
expect(currentNotifications()).toHaveLength(2);
|
|
136
|
-
expect(currentNotifications()[1]).toEqual(expect.objectContaining({ notification_id: 102, seen: false }));
|
|
137
|
-
expect(screen.getByTestId('unread-count').textContent).toBe('1');
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('decrements unread count on a cross-tab status push and does not double-count a repeat push', async () => {
|
|
141
|
-
renderProvider();
|
|
142
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('2'));
|
|
143
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
144
|
-
|
|
145
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
146
|
-
type: 'notification.status',
|
|
147
|
-
notification_id: 101,
|
|
148
|
-
status: { seen: true, dismissed: false, done: false },
|
|
149
|
-
}) });
|
|
150
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('1'));
|
|
151
|
-
|
|
152
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
153
|
-
type: 'notification.status',
|
|
154
|
-
notification_id: 101,
|
|
155
|
-
status: { seen: true, dismissed: false, done: true },
|
|
156
|
-
}) });
|
|
157
|
-
await waitFor(() => expect(currentNotifications()[0]).toEqual(expect.objectContaining({ done: true })));
|
|
158
|
-
expect(screen.getByTestId('unread-count').textContent).toBe('1');
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('constructs only one WebSocket across a provider re-render', async () => {
|
|
162
|
-
const { rerender } = renderProvider();
|
|
163
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
164
|
-
|
|
165
|
-
rerender(
|
|
166
|
-
<AuthContext.Provider value={{ user: { id: 4 } }}>
|
|
167
|
-
<NotificationsProvider><Consumer /></NotificationsProvider>
|
|
168
|
-
</AuthContext.Provider>,
|
|
169
|
-
);
|
|
170
|
-
|
|
171
|
-
expect(MockWebSocket.instances).toHaveLength(1);
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('closes the socket on unmount', async () => {
|
|
175
|
-
const { unmount } = renderProvider();
|
|
176
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
177
|
-
const socket = MockWebSocket.instances[0];
|
|
178
|
-
|
|
179
|
-
unmount();
|
|
180
|
-
|
|
181
|
-
expect(socket.close).toHaveBeenCalledOnce();
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('marks rows optimistically and sends each canonical action', async () => {
|
|
185
|
-
renderProvider();
|
|
186
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('2'));
|
|
187
|
-
|
|
188
|
-
fireEvent.click(screen.getByRole('button', { name: 'seen' }));
|
|
189
|
-
expect(currentNotifications()[0]).toEqual(expect.objectContaining({ seen: true }));
|
|
190
|
-
expect(screen.getByTestId('unread-count').textContent).toBe('1');
|
|
191
|
-
expect(feedApi.markNotifications).toHaveBeenCalledWith({ action: 'seen', ids: [1] });
|
|
192
|
-
|
|
193
|
-
fireEvent.click(screen.getByRole('button', { name: 'dismissed' }));
|
|
194
|
-
expect(currentNotifications()[1]).toEqual(expect.objectContaining({ dismissed: true }));
|
|
195
|
-
expect(screen.getByTestId('unread-count').textContent).toBe('0');
|
|
196
|
-
expect(feedApi.markNotifications).toHaveBeenCalledWith({ action: 'dismissed', ids: [2] });
|
|
197
|
-
|
|
198
|
-
fireEvent.click(screen.getByRole('button', { name: 'done' }));
|
|
199
|
-
expect(currentNotifications()[0]).toEqual(expect.objectContaining({ done: true }));
|
|
200
|
-
expect(feedApi.markNotifications).toHaveBeenCalledWith({ action: 'done', ids: [1] });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('folds two WS messages for the same notification_id into one feed entry with a single unread increment (NOTIF-12 D2)', async () => {
|
|
204
|
-
renderProvider();
|
|
205
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('2'));
|
|
206
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
207
|
-
|
|
208
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
209
|
-
type: 'payment_due',
|
|
210
|
-
channel: 'chip',
|
|
211
|
-
notification_id: 404,
|
|
212
|
-
content: { title_key: 'chip.title' },
|
|
213
|
-
}) });
|
|
214
|
-
await waitFor(() => expect(screen.getByTestId('unread-count').textContent).toBe('3'));
|
|
215
|
-
|
|
216
|
-
MockWebSocket.instances[0].onmessage({ data: JSON.stringify({
|
|
217
|
-
type: 'payment_due',
|
|
218
|
-
channel: 'popup',
|
|
219
|
-
notification_id: 404,
|
|
220
|
-
content: { title_key: 'popup.title' },
|
|
221
|
-
}) });
|
|
222
|
-
|
|
223
|
-
await waitFor(() => expect(currentNotifications().find((n) => n.notification_id === 404).content)
|
|
224
|
-
.toEqual({ title_key: 'popup.title' }));
|
|
225
|
-
expect(currentNotifications().filter((n) => n.notification_id === 404)).toHaveLength(1);
|
|
226
|
-
expect(screen.getByTestId('unread-count').textContent).toBe('3');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it('NOTIF-13 R1 guard: does not open the socket until the REST seed resolves', async () => {
|
|
230
|
-
let resolveFeed;
|
|
231
|
-
feedApi.getNotificationFeed.mockReturnValue(new Promise((resolve) => { resolveFeed = resolve; }));
|
|
232
|
-
|
|
233
|
-
renderProvider();
|
|
234
|
-
|
|
235
|
-
expect(MockWebSocket.instances).toHaveLength(0);
|
|
236
|
-
|
|
237
|
-
resolveFeed({ results: [] });
|
|
238
|
-
await waitFor(() => expect(MockWebSocket.instances).toHaveLength(1));
|
|
239
|
-
});
|
|
240
|
-
});
|