@micha.bigler/ui-core-micha 3.14.4 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +29 -0
- package/README.md +7 -0
- package/dist/chat/kitSxRegistry.js +9 -0
- package/dist/components/charts/kitSxRegistry.js +6 -0
- package/dist/entries/charts.js +6 -0
- package/dist/entries/chat.js +8 -0
- package/dist/entries/markdown.js +1 -0
- package/dist/index.js +0 -14
- package/dist/pages/AccountPage.js +1 -2
- package/dist/pages/LoginPage.js +1 -2
- package/dist/pages/PasswordChangePage.js +1 -2
- package/dist/pages/PasswordInvitePage.js +1 -2
- package/dist/pages/PasswordResetRequestPage.js +1 -2
- package/dist/pages/SignUpPage.js +1 -2
- package/dist/pages/SignupConfirmPage.js +1 -2
- package/dist/theme/kitSxEntry.js +11 -0
- package/dist/theme/kitSxRegistry.js +2 -15
- package/package.json +31 -7
- package/.github/workflows/ci.yml +0 -35
- package/.github/workflows/publish.yml +0 -126
- package/DESIGN.md +0 -60
- package/WORK_ORDERS.md +0 -150
- package/dev/entries.jsx +0 -721
- package/dev/fixtures.js +0 -26
- package/dev/main.jsx +0 -75
- package/dev/mockTransport.jsx +0 -128
- package/docs/CHART-LAYOUT.md +0 -157
- package/docs/DARK-TOKENS.md +0 -140
- package/docs/TYPE-SCALE.md +0 -111
- package/docs/messaging-deviations.md +0 -118
- package/index.html +0 -12
- package/renovate.json +0 -10
- package/scripts/chart-api-census.mjs +0 -415
- package/src/auth/AuthContext.jsx +0 -158
- package/src/auth/ReauthModal.jsx +0 -82
- package/src/auth/UserMenu.jsx +0 -121
- package/src/auth/apiClient.jsx +0 -232
- package/src/auth/authApi.jsx +0 -539
- package/src/auth/authConfig.jsx +0 -34
- package/src/auth/reauth.js +0 -47
- package/src/chat/ChatComposer.jsx +0 -36
- package/src/chat/ChatList.jsx +0 -87
- package/src/chat/ChatThread.jsx +0 -128
- package/src/chat/ChatWorkspace.jsx +0 -244
- package/src/chat/MarkdownReply.jsx +0 -22
- package/src/chat/ModelPicker.jsx +0 -85
- package/src/chat/api.js +0 -18
- package/src/chat/useChatSocket.js +0 -32
- package/src/chat/wire.js +0 -7
- package/src/components/AccessCodeManager.jsx +0 -288
- package/src/components/AccessCodeSingleUseToggle.jsx +0 -92
- package/src/components/AllowedEmailDomainsManager.jsx +0 -92
- package/src/components/AuthFactorRequirementCard.jsx +0 -95
- package/src/components/BulkInviteCsvTab.jsx +0 -198
- package/src/components/EmptyState.jsx +0 -38
- package/src/components/LoginForm.jsx +0 -159
- package/src/components/MFAComponent.jsx +0 -350
- package/src/components/MfaLoginComponent.jsx +0 -252
- package/src/components/MobileBottomNav.jsx +0 -82
- package/src/components/PasskeysComponent.jsx +0 -252
- package/src/components/PasswordChangeForm.jsx +0 -87
- package/src/components/PasswordResetRequestForm.jsx +0 -43
- package/src/components/PasswordSetForm.jsx +0 -97
- package/src/components/ProfileComponent.jsx +0 -348
- package/src/components/QrSignupManager.jsx +0 -404
- package/src/components/QrSignupValidityManager.jsx +0 -106
- package/src/components/RegistrationMethodsManager.jsx +0 -150
- package/src/components/SecurityComponent.jsx +0 -141
- package/src/components/SocialLoginButtons.jsx +0 -83
- package/src/components/SoftChip.jsx +0 -93
- package/src/components/StatTile.jsx +0 -39
- package/src/components/SupportRecoveryRequestsTab.jsx +0 -482
- package/src/components/TurnstileWidget.jsx +0 -99
- package/src/components/UserInviteComponent.jsx +0 -76
- package/src/components/UserListComponent.jsx +0 -618
- package/src/components/charts/BarChart.jsx +0 -102
- package/src/components/charts/ChartFrame.jsx +0 -214
- package/src/components/charts/LineChart.jsx +0 -126
- package/src/components/charts/ScatterChart.jsx +0 -299
- package/src/components/charts/TimeSeriesChart.jsx +0 -300
- package/src/components/charts/chartDefaults.js +0 -644
- package/src/components/charts/chartLabels.js +0 -36
- package/src/components/charts/exportChart.js +0 -258
- package/src/components/charts/formatters.js +0 -39
- package/src/components/charts/palette.js +0 -33
- package/src/components/charts/yearTickInterval.js +0 -7
- package/src/i18n/authTranslations.ts +0 -2651
- package/src/i18n/chartsTranslations.ts +0 -91
- package/src/i18n/chatTranslations.ts +0 -55
- package/src/i18n/messagingTranslations.ts +0 -120
- package/src/i18n/notificationsTranslations.ts +0 -32
- package/src/i18n/onboardingTranslations.ts +0 -29
- package/src/i18n/sectionNavTranslations.ts +0 -20
- package/src/i18n/uiCoreTranslations.js +0 -29
- package/src/i18n/userMenuTranslations.ts +0 -5
- package/src/index.js +0 -158
- package/src/layout/PageLayout.jsx +0 -36
- package/src/layout/SectionNav.jsx +0 -320
- package/src/messaging/AttachmentList.jsx +0 -145
- package/src/messaging/Composer.jsx +0 -197
- package/src/messaging/ConversationLaunchers.jsx +0 -51
- package/src/messaging/ConversationList.jsx +0 -109
- package/src/messaging/DirectMessageLauncher.jsx +0 -112
- package/src/messaging/MessageBubble.jsx +0 -183
- package/src/messaging/MessagingProvider.jsx +0 -0
- package/src/messaging/MessagingScopeConfig.jsx +0 -24
- package/src/messaging/PollCard.jsx +0 -163
- package/src/messaging/ReactionBar.jsx +0 -43
- package/src/messaging/ReadTicks.jsx +0 -82
- package/src/messaging/Thread.jsx +0 -195
- package/src/messaging/api.js +0 -48
- package/src/messaging/composerImageCompression.js +0 -35
- package/src/messaging/linkifyText.jsx +0 -37
- package/src/notifications/NotificationBell.jsx +0 -84
- package/src/notifications/NotificationSettings.jsx +0 -330
- package/src/notifications/NotificationsProvider.jsx +0 -224
- package/src/notifications/PopupSurface.jsx +0 -105
- package/src/notifications/api.js +0 -55
- package/src/notifications/feedApi.js +0 -18
- package/src/notifications/realtime.jsx +0 -138
- package/src/notifications/serviceWorker/sw.js +0 -50
- package/src/onboarding/OnboardingProvider.jsx +0 -326
- package/src/onboarding/OnboardingWizard.jsx +0 -61
- package/src/onboarding/WizardDialogShell.jsx +0 -51
- package/src/onboarding/api.js +0 -13
- package/src/onboarding/stepSelection.js +0 -16
- package/src/onboarding/steps/BrowserPushStep.jsx +0 -121
- package/src/onboarding/steps/CompleteNameStep.jsx +0 -46
- package/src/onboarding/steps/CookieConsentStep.jsx +0 -133
- package/src/onboarding/steps/PwaInstallStep.jsx +0 -47
- package/src/pages/AccountPage.jsx +0 -394
- package/src/pages/LoginPage.jsx +0 -343
- package/src/pages/PasswordChangePage.jsx +0 -62
- package/src/pages/PasswordInvitePage.jsx +0 -177
- package/src/pages/PasswordResetRequestPage.jsx +0 -68
- package/src/pages/SignUpPage.jsx +0 -268
- package/src/pages/SignupConfirmPage.jsx +0 -155
- package/src/testing/checkKitIntegration.js +0 -233
- package/src/theme/createAppTheme.js +0 -423
- package/src/theme/fonts.js +0 -3
- package/src/theme/index.js +0 -21
- package/src/theme/kitSxRegistry.js +0 -285
- package/src/theme/themeCompleteness.js +0 -1045
- package/src/theme/tokens.js +0 -478
- package/src/utils/auth-errors.js +0 -76
- package/src/utils/authService.js +0 -223
- package/src/utils/resolveErrorText.js +0 -28
- package/src/utils/webauthn.js +0 -51
- package/tests/AccountPage.profileStatements.test.jsx +0 -162
- package/tests/AccountPage.test.jsx +0 -200
- package/tests/AuthContext.test.jsx +0 -129
- package/tests/BarChart.test.jsx +0 -112
- package/tests/BrowserPushStep.test.jsx +0 -110
- package/tests/ChartFrame.test.jsx +0 -336
- package/tests/ConversationList.test.jsx +0 -144
- package/tests/CookieConsentStep.test.jsx +0 -69
- package/tests/EmptyState.test.jsx +0 -62
- package/tests/LineChart.test.jsx +0 -80
- package/tests/MessagingProvider.test.jsx +0 -50
- package/tests/MobileBottomNav.test.jsx +0 -319
- package/tests/NotificationBell.test.jsx +0 -60
- package/tests/NotificationSettings.test.jsx +0 -276
- package/tests/NotificationsProvider.test.jsx +0 -240
- package/tests/OnboardingProvider.test.jsx +0 -294
- package/tests/OnboardingWizard.test.jsx +0 -143
- package/tests/PasswordChangeForm.test.jsx +0 -70
- package/tests/PasswordInvitePage.test.jsx +0 -142
- package/tests/PasswordSetForm.test.jsx +0 -70
- package/tests/PopupSurface.test.jsx +0 -142
- package/tests/ProfileComponent.refresh.test.jsx +0 -194
- package/tests/QrSignupManager.registrationContext.test.jsx +0 -106
- package/tests/QrSignupManagerInfoBox.test.jsx +0 -49
- package/tests/ScatterChart.test.jsx +0 -270
- package/tests/SectionNav.test.jsx +0 -363
- package/tests/SignUpPage.test.jsx +0 -208
- package/tests/SoftChip.test.jsx +0 -105
- package/tests/StatTile.test.jsx +0 -57
- package/tests/Thread.test.jsx +0 -151
- package/tests/TimeSeriesChart.test.jsx +0 -367
- package/tests/UserListComponent.test.jsx +0 -192
- package/tests/UserMenu.test.jsx +0 -171
- package/tests/apiClientLanguage.test.js +0 -33
- package/tests/apiClientPublicPaths.test.js +0 -178
- package/tests/authApiLogin.test.js +0 -42
- package/tests/authApiRegistration.test.js +0 -31
- package/tests/authErrorCodeRendering.test.jsx +0 -77
- package/tests/authErrors.test.js +0 -74
- package/tests/authTranslations.test.js +0 -74
- package/tests/chart-api-census.test.js +0 -366
- package/tests/chartAxisTickBand.test.jsx +0 -95
- package/tests/chartDefaults.test.js +0 -902
- package/tests/chartLabels.test.js +0 -50
- package/tests/chartsPalette.test.js +0 -99
- package/tests/chatApi.test.js +0 -48
- package/tests/chatDriverNames.test.jsx +0 -71
- package/tests/chatExports.test.js +0 -3
- package/tests/chatList.test.jsx +0 -84
- package/tests/chatPicker.test.jsx +0 -82
- package/tests/chatSocket.test.jsx +0 -26
- package/tests/chatThread.test.jsx +0 -116
- package/tests/chatTranslations.test.js +0 -47
- package/tests/chatWorkspace.test.jsx +0 -234
- package/tests/checkKitIntegration.test.jsx +0 -219
- package/tests/createAppTheme.test.js +0 -132
- package/tests/createAppThemeDark.test.js +0 -127
- package/tests/createAppThemeLightGuard.test.js +0 -40
- package/tests/createAppThemePageContrast.test.js +0 -108
- package/tests/createAppThemeTypography.test.js +0 -53
- package/tests/errorCallSiteGuard.test.js +0 -32
- package/tests/exportChart.test.js +0 -313
- package/tests/feedApi.test.js +0 -46
- package/tests/fixtures/light-theme-baseline.json +0 -2234
- package/tests/i18nAggregate.test.js +0 -120
- package/tests/i18nGermanVariant.test.js +0 -65
- package/tests/loginPageErrorResolution.test.jsx +0 -102
- package/tests/messagingApi.test.js +0 -32
- package/tests/messagingChunk6.test.jsx +0 -116
- package/tests/messagingCompactBubble.test.jsx +0 -64
- package/tests/messagingComposer.test.jsx +0 -136
- package/tests/messagingComposerChunk5.test.jsx +0 -113
- package/tests/messagingContractConformance.test.js +0 -69
- package/tests/messagingDirectMessageLauncher.test.jsx +0 -151
- package/tests/messagingExports.test.js +0 -21
- package/tests/messagingInteractions.test.jsx +0 -136
- package/tests/messagingLastMessagePreview.test.jsx +0 -93
- package/tests/messagingLinkify.test.jsx +0 -51
- package/tests/messagingMessageActions.test.jsx +0 -163
- package/tests/messagingMsg6.test.jsx +0 -105
- package/tests/messagingMsg6f.test.jsx +0 -155
- package/tests/messagingMsg6g.test.jsx +0 -166
- package/tests/messagingMsg6h.test.jsx +0 -136
- package/tests/messagingMsg6i.test.jsx +0 -129
- package/tests/messagingPollRendering.test.jsx +0 -142
- package/tests/messagingReactionMerge.test.jsx +0 -77
- package/tests/messagingReadTicksTooltip.test.jsx +0 -36
- package/tests/messagingThreadReceipts.test.jsx +0 -178
- package/tests/messagingThreadingQuoting.test.jsx +0 -87
- package/tests/messagingUnreadLifecycle.test.jsx +0 -78
- package/tests/notificationsApi.test.js +0 -41
- package/tests/notificationsExports.test.js +0 -34
- package/tests/onboardingDescriptors.test.js +0 -132
- package/tests/packageTreeShaking.test.js +0 -126
- package/tests/realtime.test.jsx +0 -219
- package/tests/resolveErrorText.test.js +0 -72
- package/tests/stepSelection.test.js +0 -53
- package/tests/themeCompleteness.test.js +0 -626
- package/tests/timeSeriesLegend.test.js +0 -99
- package/tests/userScreenTranslations.test.jsx +0 -218
- package/tsconfig.build.json +0 -18
- package/vite.config.mjs +0 -6
- package/work-orders/AUTH-1.md +0 -74
- package/work-orders/AUTH-2.md +0 -74
- package/work-orders/AUTH-4.md +0 -183
- package/work-orders/AUTH-5.md +0 -154
- package/work-orders/AUTH-7.md +0 -181
- package/work-orders/AUTH-8.md +0 -184
- package/work-orders/CHART-1.md +0 -285
- package/work-orders/CHART-2.md +0 -222
- package/work-orders/CHART-3.md +0 -163
- package/work-orders/CHART-4.md +0 -203
- package/work-orders/CHART-5.md +0 -226
- package/work-orders/CHART-6.md +0 -428
- package/work-orders/CHART-7.md +0 -175
- package/work-orders/DX-1.md +0 -169
- package/work-orders/DX-2.md +0 -188
- package/work-orders/DX-3.md +0 -195
- package/work-orders/I18N-1.md +0 -254
- package/work-orders/I18N-3.md +0 -207
- package/work-orders/MSG-3.md +0 -375
- package/work-orders/MSG-3b.md +0 -1002
- package/work-orders/MSG-3c.md +0 -378
- package/work-orders/MSG-3d.md +0 -268
- package/work-orders/MSG-3e.md +0 -164
- package/work-orders/MSG-3f.md +0 -391
- package/work-orders/MSG-6e.md +0 -308
- package/work-orders/MSG-6f.md +0 -214
- package/work-orders/MSG-6g.md +0 -244
- package/work-orders/MSG-6h.md +0 -238
- package/work-orders/MSG-6i.md +0 -271
- package/work-orders/MSG-6j.md +0 -117
- package/work-orders/NOTIF-12.md +0 -214
- package/work-orders/NOTIF-13.md +0 -142
- package/work-orders/NOTIF-14.md +0 -151
- package/work-orders/PRIM-1.md +0 -177
- package/work-orders/SHELL-1.md +0 -255
- package/work-orders/SHELL-2.md +0 -197
- package/work-orders/SHELL-3.md +0 -359
- package/work-orders/SHELL-4.md +0 -351
- package/work-orders/SHELL-5.md +0 -345
- package/work-orders/SHELL-6.md +0 -210
- package/work-orders/THEME-1.md +0 -587
- package/work-orders/THEME-10.md +0 -303
- package/work-orders/THEME-11.md +0 -208
- package/work-orders/THEME-2.md +0 -339
- package/work-orders/THEME-3.md +0 -210
- package/work-orders/THEME-4.md +0 -271
- package/work-orders/THEME-5.md +0 -341
- package/work-orders/THEME-6.md +0 -247
- package/work-orders/THEME-7.md +0 -82
- package/work-orders/THEME-8.md +0 -211
- package/work-orders/THEME-9.md +0 -194
- package/work-orders/UCM-AUTH-10.md +0 -490
- package/work-orders/UCM-AUTH-11.md +0 -185
- package/work-orders/UCM-AUTH-12.md +0 -98
- package/work-orders/UCM-AUTH-13.md +0 -47
- package/work-orders/UCM-AUTH-9.md +0 -207
- package/work-orders/UCM-CHART-10.md +0 -150
- package/work-orders/UCM-CHART-11.md +0 -132
- package/work-orders/UCM-CHART-12.md +0 -348
- package/work-orders/UCM-CHART-13.md +0 -122
- package/work-orders/UCM-CHART-14.md +0 -198
- package/work-orders/UCM-CHART-15.md +0 -187
- package/work-orders/UCM-CHART-16.md +0 -169
- package/work-orders/UCM-CHART-17.md +0 -195
- package/work-orders/UCM-CHART-18.md +0 -200
- package/work-orders/UCM-CHART-8.md +0 -147
- package/work-orders/UCM-CHART-9.md +0 -265
- package/work-orders/UCM-CHAT-1.md +0 -190
- package/work-orders/UCM-CHAT-2.md +0 -75
- package/work-orders/UCM-CI-1.md +0 -65
- package/work-orders/UCM-DEP-1.md +0 -63
- package/work-orders/UCM-I18N-4.md +0 -226
- package/work-orders/UCM-I18N-5.md +0 -211
- package/work-orders/UCM-MSG-20.md +0 -192
- package/work-orders/UCM-PRIM-2.md +0 -114
- package/work-orders/UCM-SHELL-7.md +0 -134
- package/work-orders/UCM-SHELL-8.md +0 -125
- package/work-orders/UCM-TEST-1.md +0 -304
- package/work-orders/UCM-THEME-13.md +0 -69
- package/work-orders/UCM-THEME-14.md +0 -312
- package/work-orders/UCM-THEME-15.md +0 -356
- package/work-orders/UCM-THEME-16.md +0 -80
- package/work-orders/UCM-THEME-17.md +0 -111
- package/work-orders/UCM-THEME-18.md +0 -184
- package/work-orders/assets/AUTH-4-user-list-card.html +0 -407
- package/work-orders/assets/PRIM-1-dashboard-primitives.html +0 -624
- package/work-orders/assets/SHELL-2-account-navigation.html +0 -1159
- package/work-orders/assets/THEME-1-baseline-reference-sheet.html +0 -1352
- package/work-orders/assets/UCM-CHAT-1-ai-chat.html +0 -447
- package/work-orders/assets/UCM-THEME-13-dark-prototype.html +0 -390
- package/work-orders/assets/UCM-THEME-17-type-scale.html +0 -391
|
@@ -1,234 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';
|
|
5
|
-
|
|
6
|
-
vi.mock('react-i18next', () => ({ useTranslation: () => ({ t: (key, values) => (values?.model !== undefined ? `${key}:${values.model}` : key) }) }));
|
|
7
|
-
vi.mock('@mui/icons-material', () => ({ MoreVert: () => null }));
|
|
8
|
-
|
|
9
|
-
import ChatWorkspace from '../src/chat/ChatWorkspace';
|
|
10
|
-
|
|
11
|
-
class FakeSocket {
|
|
12
|
-
static OPEN = 1;
|
|
13
|
-
static instances = [];
|
|
14
|
-
constructor(url) { this.url = url; this.readyState = 0; this.sent = []; FakeSocket.instances.push(this); }
|
|
15
|
-
open() { this.readyState = 1; this.onopen?.(); }
|
|
16
|
-
push(frame) { this.onmessage?.({ data: JSON.stringify(frame) }); }
|
|
17
|
-
send(data) { this.sent.push(JSON.parse(data)); }
|
|
18
|
-
close(code = 1000) { this.readyState = 3; this.onclose?.({ code }); }
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const catalogue = {
|
|
22
|
-
entries: [
|
|
23
|
-
{ id: 'cli-sonnet', driver: 'claude_cli', model: 'sonnet', label: 'Sonnet', efforts: ['low', 'high'], billing: 'subscription' },
|
|
24
|
-
{ id: 'oa', driver: 'openai_api', model: 'gpt', label: 'GPT', efforts: ['medium'], billing: 'pay_per_use' },
|
|
25
|
-
],
|
|
26
|
-
default: { entry: 'cli-sonnet', effort: 'high' },
|
|
27
|
-
};
|
|
28
|
-
const baseChat = { id: 7, title: 'Budget', archived_at: null, selection: { entry: 'cli-sonnet', effort: 'high' } };
|
|
29
|
-
|
|
30
|
-
function makeApi(extra = {}) {
|
|
31
|
-
return {
|
|
32
|
-
getStatus: vi.fn().mockResolvedValue({ status: 'available' }),
|
|
33
|
-
getCatalogue: vi.fn().mockResolvedValue(catalogue),
|
|
34
|
-
listChats: vi.fn().mockImplementation(async ({ archived } = {}) => ({ results: archived ? [] : [baseChat], next: null })),
|
|
35
|
-
createChat: vi.fn().mockResolvedValue({ ...baseChat, id: 8, title: '' }),
|
|
36
|
-
updateChat: vi.fn().mockImplementation(async (id, patch) => ({ ...baseChat, id, ...patch })),
|
|
37
|
-
deleteChat: vi.fn().mockResolvedValue(null),
|
|
38
|
-
listTurns: vi.fn().mockResolvedValue({ results: [], next: null }),
|
|
39
|
-
...extra,
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
async function openChat(api) {
|
|
44
|
-
render(<ChatWorkspace api={api} wsUrl={(id) => `ws://test/${id}`} />);
|
|
45
|
-
fireEvent.click(await screen.findByText('Budget'));
|
|
46
|
-
await waitFor(() => expect(FakeSocket.instances.length).toBe(1));
|
|
47
|
-
const socket = FakeSocket.instances[0];
|
|
48
|
-
act(() => socket.open());
|
|
49
|
-
return socket;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
beforeEach(() => { globalThis.WebSocket = FakeSocket; });
|
|
53
|
-
afterEach(() => { cleanup(); delete globalThis.WebSocket; FakeSocket.instances = []; });
|
|
54
|
-
|
|
55
|
-
describe('ChatWorkspace streaming', () => {
|
|
56
|
-
it('streams deltas into the answer and replaces them with the final turn and its sources', async () => {
|
|
57
|
-
const socket = await openChat(makeApi());
|
|
58
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.message' }), { target: { value: 'Hello' } });
|
|
59
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.send' }));
|
|
60
|
-
expect(socket.sent).toEqual([{ type: 'send', message: 'Hello' }]);
|
|
61
|
-
expect(screen.getByText('Hello')).toBeTruthy();
|
|
62
|
-
expect(screen.getByRole('button', { name: 'Chat.stop' })).toBeTruthy();
|
|
63
|
-
expect(screen.getByRole('button', { name: 'Chat.send' }).disabled).toBe(true);
|
|
64
|
-
|
|
65
|
-
act(() => socket.push({ type: 'tool', phase: 'start', name: 'kb', label: 'Searching the knowledge base' }));
|
|
66
|
-
expect(screen.getByText('Searching the knowledge base')).toBeTruthy();
|
|
67
|
-
act(() => { socket.push({ type: 'delta', text: 'Par' }); socket.push({ type: 'delta', text: 'tial' }); });
|
|
68
|
-
expect(screen.getByText('Partial')).toBeTruthy();
|
|
69
|
-
|
|
70
|
-
act(() => socket.push({ type: 'turn', turn: { id: 40, seq: 1, user_text: 'Hello', assistant_text: 'Final text', state: 'completed', sources: [{ label: 'KB-1', reference: 'kb://1' }], driver: 'claude_cli', model: 'Sonnet', effort: 'high' } }));
|
|
71
|
-
expect(screen.queryByText('Partial')).toBeNull();
|
|
72
|
-
expect(screen.getByText('Final text')).toBeTruthy();
|
|
73
|
-
expect(screen.getByText('KB-1')).toBeTruthy();
|
|
74
|
-
expect(screen.queryByText('Searching the knowledge base')).toBeNull();
|
|
75
|
-
expect(screen.getByRole('button', { name: 'Chat.send' })).toBeTruthy();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('sends stop over the socket while a turn runs', async () => {
|
|
79
|
-
const socket = await openChat(makeApi());
|
|
80
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.message' }), { target: { value: 'Hello' } });
|
|
81
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.send' }));
|
|
82
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.stop' }));
|
|
83
|
-
expect(socket.sent.at(-1)).toEqual({ type: 'stop' });
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('does not resend anything after a reconnect', async () => {
|
|
87
|
-
vi.useFakeTimers({ shouldAdvanceTime: true });
|
|
88
|
-
try {
|
|
89
|
-
const socket = await openChat(makeApi());
|
|
90
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.message' }), { target: { value: 'Hello' } });
|
|
91
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.send' }));
|
|
92
|
-
act(() => socket.close(1006));
|
|
93
|
-
act(() => { vi.advanceTimersByTime(1000); });
|
|
94
|
-
expect(FakeSocket.instances).toHaveLength(2);
|
|
95
|
-
act(() => FakeSocket.instances[1].open());
|
|
96
|
-
expect(FakeSocket.instances[1].sent).toEqual([]);
|
|
97
|
-
} finally { vi.useRealTimers(); }
|
|
98
|
-
});
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
describe('ChatWorkspace edge states', () => {
|
|
102
|
-
it('shows the usage-limit state with Change model, which opens the picker', async () => {
|
|
103
|
-
const socket = await openChat(makeApi());
|
|
104
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.message' }), { target: { value: 'Hello' } });
|
|
105
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.send' }));
|
|
106
|
-
act(() => socket.push({ type: 'error', code: 'quota_exceeded' }));
|
|
107
|
-
expect(screen.getByText('Chat.usageLimit:Sonnet')).toBeTruthy();
|
|
108
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.changeModel' }));
|
|
109
|
-
expect(screen.getByRole('group', { name: 'Chat.driver.openai_api' })).toBeTruthy();
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('shows an interrupted turn from history with Retry, which sends its text again only on click', async () => {
|
|
113
|
-
const api = makeApi({ listTurns: vi.fn().mockResolvedValue({ results: [{ id: 5, seq: 1, user_text: 'Again please', assistant_text: 'Half', state: 'interrupted', sources: [], driver: 'claude_cli', model: 'Sonnet', effort: 'high' }], next: null }) });
|
|
114
|
-
const socket = await openChat(api);
|
|
115
|
-
await screen.findByText('Chat.interrupted');
|
|
116
|
-
expect(socket.sent).toEqual([]);
|
|
117
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.retry' }));
|
|
118
|
-
expect(socket.sent).toEqual([{ type: 'send', message: 'Again please' }]);
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('shows Reconnecting while the connection is being re-established and blocks Send', async () => {
|
|
122
|
-
const socket = await openChat(makeApi());
|
|
123
|
-
act(() => socket.close(1006));
|
|
124
|
-
expect(screen.getByText('Chat.reconnecting')).toBeTruthy();
|
|
125
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.message' }), { target: { value: 'x' } });
|
|
126
|
-
expect(screen.getByRole('button', { name: 'Chat.send' }).disabled).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it.each([[4401, 'Chat.sessionEnded'], [4403, 'Chat.accessLost'], [4429, 'Chat.rateLimited']])('stops reconnecting on close code %s and shows its state', async (code, text) => {
|
|
130
|
-
const socket = await openChat(makeApi());
|
|
131
|
-
act(() => socket.close(code));
|
|
132
|
-
expect(screen.getByText(text)).toBeTruthy();
|
|
133
|
-
expect(FakeSocket.instances).toHaveLength(1);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it('shows the not-configured state and loads no chat list', async () => {
|
|
137
|
-
const api = makeApi({ getStatus: vi.fn().mockResolvedValue({ status: 'not_configured' }) });
|
|
138
|
-
render(<ChatWorkspace api={api} />);
|
|
139
|
-
expect(await screen.findByText('Chat.unavailable')).toBeTruthy();
|
|
140
|
-
expect(screen.getByText('Chat.noModel')).toBeTruthy();
|
|
141
|
-
expect(screen.queryByRole('button', { name: 'Chat.newChat' })).toBeNull();
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('opens an archived chat read-only: Send is disabled and Restore is offered', async () => {
|
|
145
|
-
const archivedChat = { ...baseChat, archived_at: '2026-10-01T00:00:00Z' };
|
|
146
|
-
const api = makeApi({ listChats: vi.fn().mockImplementation(async ({ archived } = {}) => ({ results: archived ? [archivedChat] : [], next: null })) });
|
|
147
|
-
render(<ChatWorkspace api={api} wsUrl={(id) => `ws://test/${id}`} />);
|
|
148
|
-
fireEvent.click(await screen.findByRole('button', { name: /Chat.archived/ }));
|
|
149
|
-
fireEvent.click(screen.getByText('Budget'));
|
|
150
|
-
expect(await screen.findByText('Chat.archivedReadOnly')).toBeTruthy();
|
|
151
|
-
expect(screen.getByRole('textbox', { name: 'Chat.message' }).disabled).toBe(true);
|
|
152
|
-
expect(screen.getByRole('button', { name: 'Chat.send' }).disabled).toBe(true);
|
|
153
|
-
fireEvent.click(screen.getAllByRole('button', { name: 'Chat.restore' })[0]);
|
|
154
|
-
await waitFor(() => expect(api.updateChat).toHaveBeenCalledWith(7, { archived: false }));
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
it('shows an empty new chat', async () => {
|
|
158
|
-
const api = makeApi();
|
|
159
|
-
render(<ChatWorkspace api={api} wsUrl={(id) => `ws://test/${id}`} />);
|
|
160
|
-
fireEvent.click((await screen.findAllByRole('button', { name: 'Chat.newChat' }))[0]);
|
|
161
|
-
expect(await screen.findByText('Chat.emptyTitle')).toBeTruthy();
|
|
162
|
-
expect(api.createChat).toHaveBeenCalled();
|
|
163
|
-
});
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
describe('ChatWorkspace load order', () => {
|
|
167
|
-
it('shows no Reconnecting chip on the first connect', async () => {
|
|
168
|
-
render(<ChatWorkspace api={makeApi()} wsUrl={(id) => `ws://test/${id}`} />);
|
|
169
|
-
fireEvent.click(await screen.findByText('Budget'));
|
|
170
|
-
await waitFor(() => expect(FakeSocket.instances.length).toBe(1));
|
|
171
|
-
expect(screen.queryByText('Chat.reconnecting')).toBeNull();
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
it('does not request catalogue or chat lists when no model is configured', async () => {
|
|
175
|
-
const api = makeApi({ getStatus: vi.fn().mockResolvedValue({ status: 'not_configured' }) });
|
|
176
|
-
render(<ChatWorkspace api={api} />);
|
|
177
|
-
await screen.findByText('Chat.unavailable');
|
|
178
|
-
expect(api.getCatalogue).not.toHaveBeenCalled();
|
|
179
|
-
expect(api.listChats).not.toHaveBeenCalled();
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('ChatWorkspace wiring', () => {
|
|
184
|
-
it('calls the update endpoint when the picker selection changes', async () => {
|
|
185
|
-
const api = makeApi();
|
|
186
|
-
await openChat(api);
|
|
187
|
-
fireEvent.click(screen.getByRole('button', { name: 'Sonnet · Chat.effort.high' }));
|
|
188
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.effort.low' }));
|
|
189
|
-
await waitFor(() => expect(api.updateChat).toHaveBeenCalledWith(7, { selection: { entry: 'cli-sonnet', effort: 'low' } }));
|
|
190
|
-
});
|
|
191
|
-
it('wires rename, archive and delete of a chat to the endpoints', async () => {
|
|
192
|
-
const api = makeApi();
|
|
193
|
-
render(<ChatWorkspace api={api} wsUrl={(id) => `ws://test/${id}`} />);
|
|
194
|
-
await screen.findByText('Budget');
|
|
195
|
-
const menu = () => fireEvent.click(screen.getAllByRole('button', { name: 'Chat.actions' })[0]);
|
|
196
|
-
menu();
|
|
197
|
-
fireEvent.click(screen.getByRole('menuitem', { name: 'Chat.rename' }));
|
|
198
|
-
fireEvent.change(screen.getByRole('textbox', { name: 'Chat.renameLabel' }), { target: { value: 'Plan' } });
|
|
199
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.save' }));
|
|
200
|
-
await waitFor(() => expect(api.updateChat).toHaveBeenCalledWith(7, { title: 'Plan' }));
|
|
201
|
-
await waitFor(() => expect(screen.queryByRole('dialog')).toBeNull());
|
|
202
|
-
menu();
|
|
203
|
-
fireEvent.click(screen.getByRole('menuitem', { name: 'Chat.archive' }));
|
|
204
|
-
await waitFor(() => expect(api.updateChat).toHaveBeenCalledWith(7, { archived: true }));
|
|
205
|
-
await waitFor(() => expect(screen.queryByRole('menu')).toBeNull());
|
|
206
|
-
menu();
|
|
207
|
-
fireEvent.click(screen.getByRole('menuitem', { name: 'Chat.delete' }));
|
|
208
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.delete' }));
|
|
209
|
-
await waitFor(() => expect(api.deleteChat).toHaveBeenCalledWith(7));
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('offers no Retry on an archived chat', async () => {
|
|
213
|
-
const archivedChat = { ...baseChat, archived_at: '2026-10-01T00:00:00Z' };
|
|
214
|
-
const api = makeApi({
|
|
215
|
-
listChats: vi.fn().mockImplementation(async ({ archived } = {}) => ({ results: archived ? [archivedChat] : [], next: null })),
|
|
216
|
-
listTurns: vi.fn().mockResolvedValue({ results: [{ id: 5, seq: 1, user_text: 'q', assistant_text: 'half', state: 'interrupted', sources: [] }], next: null }),
|
|
217
|
-
});
|
|
218
|
-
render(<ChatWorkspace api={api} wsUrl={(id) => `ws://test/${id}`} />);
|
|
219
|
-
fireEvent.click(await screen.findByRole('button', { name: /Chat.archived/ }));
|
|
220
|
-
fireEvent.click(screen.getByText('Budget'));
|
|
221
|
-
await screen.findByText('Chat.interrupted');
|
|
222
|
-
expect(screen.queryByRole('button', { name: 'Chat.retry' })).toBeNull();
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('keeps an interrupted turn visible when Retry is clicked without a connection', async () => {
|
|
226
|
-
const api = makeApi({ listTurns: vi.fn().mockResolvedValue({ results: [{ id: 5, seq: 1, user_text: 'Again', assistant_text: 'half', state: 'interrupted', sources: [] }], next: null }) });
|
|
227
|
-
const socket = await openChat(api);
|
|
228
|
-
await screen.findByText('Chat.interrupted');
|
|
229
|
-
act(() => socket.close(4403));
|
|
230
|
-
fireEvent.click(screen.getByRole('button', { name: 'Chat.retry' }));
|
|
231
|
-
expect(screen.getByText('Again')).toBeTruthy();
|
|
232
|
-
expect(socket.sent).toEqual([]);
|
|
233
|
-
});
|
|
234
|
-
});
|
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
// @vitest-environment jsdom
|
|
2
|
-
import React from 'react';
|
|
3
|
-
import { afterEach, beforeAll, describe, expect, it, vi } from 'vitest';
|
|
4
|
-
import i18next from 'i18next';
|
|
5
|
-
import { I18nextProvider } from 'react-i18next';
|
|
6
|
-
|
|
7
|
-
import apiClient from '../src/auth/apiClient';
|
|
8
|
-
import { AuthContext } from '../src/auth/AuthContext';
|
|
9
|
-
import { checkKitIntegration } from '../src/testing/checkKitIntegration';
|
|
10
|
-
import { uiCoreTranslations } from '../src/i18n/uiCoreTranslations';
|
|
11
|
-
import { LoginPage } from '../src/pages/LoginPage';
|
|
12
|
-
|
|
13
|
-
const languages = ['de', 'en', 'fr', 'sw'];
|
|
14
|
-
const authMethods = {
|
|
15
|
-
password_login: true,
|
|
16
|
-
password_reset: false,
|
|
17
|
-
signup: false,
|
|
18
|
-
signup_modes: [],
|
|
19
|
-
social_login: false,
|
|
20
|
-
passkey_login: false,
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
function resources() {
|
|
24
|
-
return Object.fromEntries(languages.map((language) => [language, {
|
|
25
|
-
translation: Object.fromEntries(
|
|
26
|
-
Object.entries(uiCoreTranslations).map(([key, values]) => [key, values[language]]),
|
|
27
|
-
),
|
|
28
|
-
}]));
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function makeWrapper(instance, authMethodsOverride = authMethods) {
|
|
32
|
-
return function FixtureWrapper({ children }) {
|
|
33
|
-
return (
|
|
34
|
-
<I18nextProvider i18n={instance}>
|
|
35
|
-
<AuthContext.Provider value={{
|
|
36
|
-
user: null,
|
|
37
|
-
loading: false,
|
|
38
|
-
login: vi.fn(),
|
|
39
|
-
authMethods: authMethodsOverride,
|
|
40
|
-
}}>
|
|
41
|
-
{children}
|
|
42
|
-
</AuthContext.Provider>
|
|
43
|
-
</I18nextProvider>
|
|
44
|
-
);
|
|
45
|
-
};
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
beforeAll(async () => {
|
|
49
|
-
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
|
|
50
|
-
await i18next.init({
|
|
51
|
-
lng: 'en',
|
|
52
|
-
fallbackLng: false,
|
|
53
|
-
supportedLngs: languages,
|
|
54
|
-
resources: resources(),
|
|
55
|
-
keySeparator: false,
|
|
56
|
-
nsSeparator: false,
|
|
57
|
-
interpolation: { escapeValue: false },
|
|
58
|
-
});
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
afterEach(async () => {
|
|
62
|
-
await i18next.changeLanguage('en');
|
|
63
|
-
for (const language of languages) {
|
|
64
|
-
i18next.removeResourceBundle(language, 'translation');
|
|
65
|
-
i18next.addResourceBundle(language, 'translation', resources()[language].translation, true, true);
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
describe('checkKitIntegration', () => {
|
|
70
|
-
it('passes for a correctly wired app fixture', async () => {
|
|
71
|
-
const { findings } = await checkKitIntegration({
|
|
72
|
-
i18n: i18next,
|
|
73
|
-
wrapper: makeWrapper(i18next),
|
|
74
|
-
pages: [LoginPage],
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(findings).toEqual([]);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('does not flag a legitimate short lowercase translated word as a raw backend code', async () => {
|
|
81
|
-
// Auth.LOGIN_OR's German value is literally "oder" -- all-lowercase, no
|
|
82
|
-
// spaces, length 4 -- the exact shape a naive raw-code heuristic would
|
|
83
|
-
// false-positive on. Render LoginForm's sign-up/forgot-password divider
|
|
84
|
-
// (onSignUp truthy) in German to actually exercise that rendered text.
|
|
85
|
-
await i18next.changeLanguage('de');
|
|
86
|
-
const { findings } = await checkKitIntegration({
|
|
87
|
-
i18n: i18next,
|
|
88
|
-
wrapper: makeWrapper(i18next, { ...authMethods, signup: true, signup_modes: ['self_signup_open'] }),
|
|
89
|
-
pages: [LoginPage],
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
expect(findings).toEqual([]);
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('reports one catalogue finding for one missing language key', async () => {
|
|
96
|
-
const key = 'Auth.field.email.invalid';
|
|
97
|
-
const french = { ...resources().fr.translation };
|
|
98
|
-
delete french[key];
|
|
99
|
-
i18next.removeResourceBundle('fr', 'translation');
|
|
100
|
-
i18next.addResourceBundle('fr', 'translation', french, true, true);
|
|
101
|
-
|
|
102
|
-
const { findings } = await checkKitIntegration({
|
|
103
|
-
i18n: i18next,
|
|
104
|
-
wrapper: makeWrapper(i18next),
|
|
105
|
-
pages: [],
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
expect(findings).toHaveLength(1);
|
|
109
|
-
expect(findings[0]).toMatchObject({ check: 'catalogue' });
|
|
110
|
-
expect(findings[0].reason).toContain(`${key} for language fr`);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
it('still reports a missing key when the consumer app configures a fallback language', async () => {
|
|
114
|
-
// A consumer typically sets fallbackLng (e.g. "en"), not "false" like the
|
|
115
|
-
// fixture above. Without forcing fallbackLng: false per-call, i18next's
|
|
116
|
-
// exists() would resolve the missing "fr" key through the fallback and
|
|
117
|
-
// read as present, masking the real gap this check exists to catch.
|
|
118
|
-
const key = 'Auth.field.email.invalid';
|
|
119
|
-
const french = { ...resources().fr.translation };
|
|
120
|
-
delete french[key];
|
|
121
|
-
i18next.removeResourceBundle('fr', 'translation');
|
|
122
|
-
i18next.addResourceBundle('fr', 'translation', french, true, true);
|
|
123
|
-
i18next.options.fallbackLng = 'en';
|
|
124
|
-
|
|
125
|
-
try {
|
|
126
|
-
const { findings } = await checkKitIntegration({
|
|
127
|
-
i18n: i18next,
|
|
128
|
-
wrapper: makeWrapper(i18next),
|
|
129
|
-
pages: [],
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
expect(findings).toHaveLength(1);
|
|
133
|
-
expect(findings[0]).toMatchObject({ check: 'catalogue' });
|
|
134
|
-
expect(findings[0].reason).toContain(`${key} for language fr`);
|
|
135
|
-
} finally {
|
|
136
|
-
i18next.options.fallbackLng = false;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('reports when the app passes a separate i18next instance', async () => {
|
|
141
|
-
const separate = i18next.createInstance();
|
|
142
|
-
await separate.init({
|
|
143
|
-
lng: 'en',
|
|
144
|
-
fallbackLng: false,
|
|
145
|
-
supportedLngs: languages,
|
|
146
|
-
resources: resources(),
|
|
147
|
-
keySeparator: false,
|
|
148
|
-
nsSeparator: false,
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
const { findings } = await checkKitIntegration({
|
|
152
|
-
i18n: separate,
|
|
153
|
-
wrapper: makeWrapper(separate),
|
|
154
|
-
pages: [],
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
expect(findings.some(({ check }) => check === 'instance')).toBe(true);
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('reports a page when the consumer wrapper throws', async () => {
|
|
161
|
-
function ThrowingWrapper() {
|
|
162
|
-
throw new Error('provider exploded');
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const before = apiClient.defaults.adapter;
|
|
166
|
-
const { findings } = await checkKitIntegration({
|
|
167
|
-
i18n: i18next,
|
|
168
|
-
wrapper: ThrowingWrapper,
|
|
169
|
-
pages: [LoginPage],
|
|
170
|
-
});
|
|
171
|
-
|
|
172
|
-
expect(findings.some(({ check, reason }) => check === 'render' && reason.includes('LoginPage'))).toBe(true);
|
|
173
|
-
expect(apiClient.defaults.adapter).toBe(before);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('passes the exact invalid-email resolver regression through the real login page', async () => {
|
|
177
|
-
const { findings } = await checkKitIntegration({
|
|
178
|
-
i18n: i18next,
|
|
179
|
-
wrapper: makeWrapper(i18next),
|
|
180
|
-
pages: [],
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
expect(findings).toEqual([]);
|
|
184
|
-
});
|
|
185
|
-
|
|
186
|
-
it('restores the transport adapter after a successful check', async () => {
|
|
187
|
-
const before = apiClient.defaults.adapter;
|
|
188
|
-
await checkKitIntegration({
|
|
189
|
-
i18n: i18next,
|
|
190
|
-
wrapper: makeWrapper(i18next),
|
|
191
|
-
pages: [],
|
|
192
|
-
});
|
|
193
|
-
expect(apiClient.defaults.adapter).toBe(before);
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it('no longer intercepts a real request once the check has finished', async () => {
|
|
197
|
-
// Proves the restore is functional, not just reference-equal: a request
|
|
198
|
-
// made AFTER the check completes must reach the app's own adapter, not
|
|
199
|
-
// the check's stub (which would reject it as "blocked" or fake a login
|
|
200
|
-
// failure for the allauth login path).
|
|
201
|
-
const sentinel = vi.fn().mockResolvedValue({ data: 'real-response', status: 200, headers: {}, config: {} });
|
|
202
|
-
const before = apiClient.defaults.adapter;
|
|
203
|
-
apiClient.defaults.adapter = sentinel;
|
|
204
|
-
|
|
205
|
-
try {
|
|
206
|
-
await checkKitIntegration({
|
|
207
|
-
i18n: i18next,
|
|
208
|
-
wrapper: makeWrapper(i18next),
|
|
209
|
-
pages: [],
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
const response = await apiClient.get('/api/some/path');
|
|
213
|
-
expect(sentinel).toHaveBeenCalled();
|
|
214
|
-
expect(response.data).toBe('real-response');
|
|
215
|
-
} finally {
|
|
216
|
-
apiClient.defaults.adapter = before;
|
|
217
|
-
}
|
|
218
|
-
});
|
|
219
|
-
});
|
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
import { createTheme } from '@mui/material/styles';
|
|
2
|
-
import { describe, expect, it } from 'vitest';
|
|
3
|
-
|
|
4
|
-
import { assertThemeComplete, createAppTheme } from '../src/theme';
|
|
5
|
-
import { BASELINE_STATIC } from '../src/theme/tokens';
|
|
6
|
-
|
|
7
|
-
describe('createAppTheme', () => {
|
|
8
|
-
it('builds a complete baseline from only an app primary colour', () => {
|
|
9
|
-
const theme = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
10
|
-
|
|
11
|
-
expect(assertThemeComplete(theme).findings).toEqual([]);
|
|
12
|
-
expect(theme.palette.background.subtle).toBe('#F4F5F6');
|
|
13
|
-
expect(theme.typography.body1.fontSize).toBe('14px');
|
|
14
|
-
expect(theme.shape.borderRadius).toBe(3);
|
|
15
|
-
expect(theme.components.MuiPaper.defaultProps).toMatchObject({ elevation: 0, variant: 'outlined' });
|
|
16
|
-
expect(theme.components.MuiBottomNavigationAction.styleOverrides.label).toMatchObject({
|
|
17
|
-
fontSize: '12px',
|
|
18
|
-
fontWeight: 500,
|
|
19
|
-
lineHeight: 1.2,
|
|
20
|
-
maxWidth: '100%',
|
|
21
|
-
whiteSpace: 'nowrap',
|
|
22
|
-
overflow: 'hidden',
|
|
23
|
-
textOverflow: 'ellipsis',
|
|
24
|
-
'&.Mui-selected': { fontSize: '12px' },
|
|
25
|
-
});
|
|
26
|
-
expect(theme.components.MuiBottomNavigationAction.styleOverrides.root).toMatchObject({
|
|
27
|
-
padding: '0 2px',
|
|
28
|
-
gap: '2px',
|
|
29
|
-
'& .MuiSvgIcon-root': { width: '22px', height: '22px' },
|
|
30
|
-
});
|
|
31
|
-
expect(theme.components.MuiBottomNavigation.styleOverrides.root).not.toHaveProperty('borderTop');
|
|
32
|
-
expect(theme.components.MuiBottomNavigation.styleOverrides.root).not.toHaveProperty('borderColor');
|
|
33
|
-
expect(theme.components.MuiBottomNavigationAction.styleOverrides.root)
|
|
34
|
-
.not.toHaveProperty('minWidth');
|
|
35
|
-
expect(theme.components.MuiBottomNavigationAction.styleOverrides.root)
|
|
36
|
-
.not.toHaveProperty('maxWidth');
|
|
37
|
-
expect(theme.palette.dataSeries.categorical).toHaveLength(6);
|
|
38
|
-
expect(theme.palette.primary.light).not.toBe(createTheme().palette.primary.light);
|
|
39
|
-
// MUI's createTheme(options, ...args) only augments the FIRST argument's
|
|
40
|
-
// palette; every later arg is deep-merged raw, so a status entry missing
|
|
41
|
-
// `main`/`contrastText` silently keeps MUI's stock hue for color="success"
|
|
42
|
-
// etc. -- main/contrastText must be explicitly set and clear AA as a
|
|
43
|
-
// foreground (THEME-2: `warning` deliberately breaks the main===fill
|
|
44
|
-
// pattern success/error/info follow -- amber cannot serve both a legible
|
|
45
|
-
// fill AND a legible foreground with one hex, so `warning.main` is the
|
|
46
|
-
// darker `text` tone instead, `warning.fill` stays the lighter amber for
|
|
47
|
-
// deliberate fill use).
|
|
48
|
-
for (const status of ['success', 'error', 'info']) {
|
|
49
|
-
expect(theme.palette[status].main).toBe(theme.palette[status].fill);
|
|
50
|
-
expect(theme.palette[status].contrastText).toBe(theme.palette[status].fillText);
|
|
51
|
-
}
|
|
52
|
-
expect(theme.palette.warning.main).toBe(theme.palette.warning.text);
|
|
53
|
-
expect(theme.palette.warning.contrastText).toBe('#FFFFFF');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('throws when palette.primary is missing', () => {
|
|
57
|
-
expect(() => createAppTheme()).toThrow(/palette\.primary.*required/i);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('throws on a function in an app styleOverrides slot', () => {
|
|
61
|
-
expect(() => createAppTheme({
|
|
62
|
-
palette: { primary: { main: '#0F62FE' } },
|
|
63
|
-
components: { MuiButton: { styleOverrides: { root: () => ({ color: 'red' }) } } },
|
|
64
|
-
})).toThrow(/components\.MuiButton\.styleOverrides\.root.*object.*variants/i);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('computes component values from the final app status palette', () => {
|
|
68
|
-
const theme = createAppTheme({
|
|
69
|
-
palette: {
|
|
70
|
-
primary: { main: '#0F62FE' },
|
|
71
|
-
success: { main: '#14532D', text: '#14532D', bg: '#DCFCE7' },
|
|
72
|
-
},
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
expect(theme.components.MuiAlert.styleOverrides.standardSuccess).toEqual({
|
|
76
|
-
color: '#14532D',
|
|
77
|
-
backgroundColor: '#DCFCE7',
|
|
78
|
-
'& .MuiAlert-icon': { color: '#14532D' },
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('deep-merges app object overrides without dropping baseline properties', () => {
|
|
83
|
-
const theme = createAppTheme({
|
|
84
|
-
palette: { primary: { main: '#0F62FE' } },
|
|
85
|
-
typography: { fontFamily: "'App Sans', sans-serif" },
|
|
86
|
-
components: { MuiButton: { styleOverrides: { root: { fontSize: '15px' } } } },
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(theme.components.MuiButton.styleOverrides.root).toMatchObject({
|
|
90
|
-
minHeight: 40,
|
|
91
|
-
borderRadius: 3,
|
|
92
|
-
fontSize: '15px',
|
|
93
|
-
});
|
|
94
|
-
expect(theme.typography.body1.fontFamily).toBe("'App Sans', sans-serif");
|
|
95
|
-
expect(theme.components.MuiOutlinedInput.styleOverrides.root.height).toBeUndefined();
|
|
96
|
-
// variants is a sibling of styleOverrides (MUI matches it independently by
|
|
97
|
-
// props), not nested inside it -- see src/theme/tokens.js for why.
|
|
98
|
-
expect(theme.components.MuiOutlinedInput.variants).toContainEqual({
|
|
99
|
-
props: { multiline: false },
|
|
100
|
-
style: { height: 40 },
|
|
101
|
-
});
|
|
102
|
-
});
|
|
103
|
-
|
|
104
|
-
it('returns independent themes for runtime palettes in one process', () => {
|
|
105
|
-
const blue = createAppTheme({ palette: { primary: { main: '#0F62FE' } } });
|
|
106
|
-
const purple = createAppTheme({ palette: { primary: { main: '#432CA1' } } });
|
|
107
|
-
|
|
108
|
-
expect(blue).not.toBe(purple);
|
|
109
|
-
expect(blue.palette).not.toBe(purple.palette);
|
|
110
|
-
expect(blue.palette.dataSeries.categorical).not.toBe(purple.palette.dataSeries.categorical);
|
|
111
|
-
expect(blue.palette.primary.main).toBe('#0F62FE');
|
|
112
|
-
expect(purple.palette.primary.main).toBe('#432CA1');
|
|
113
|
-
expect(blue.palette.controlBorder.focus).not.toBe(purple.palette.controlBorder.focus);
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('contains no function anywhere in the baseline components tree', () => {
|
|
117
|
-
const functionPaths = [];
|
|
118
|
-
const visit = (value, path) => {
|
|
119
|
-
if (typeof value === 'function') functionPaths.push(path);
|
|
120
|
-
else if (value && typeof value === 'object') {
|
|
121
|
-
Object.entries(value).forEach(([key, child]) => visit(child, `${path}.${key}`));
|
|
122
|
-
}
|
|
123
|
-
};
|
|
124
|
-
|
|
125
|
-
visit(BASELINE_STATIC.components, 'baseline.components');
|
|
126
|
-
visit(
|
|
127
|
-
createAppTheme({ palette: { primary: { main: '#0F62FE' } } }).components,
|
|
128
|
-
'theme.components',
|
|
129
|
-
);
|
|
130
|
-
expect(functionPaths).toEqual([]);
|
|
131
|
-
});
|
|
132
|
-
});
|