chore: migrate test suite from JavaScript to TypeScript#3057
chore: migrate test suite from JavaScript to TypeScript#3057
Conversation
- Install @total-typescript/shoehorn for pragmatic test typing - Update tsconfig.test.json: broaden include, add jest-dom/vitest types - Update eslint.config.mjs: relax no-explicit-any and no-non-null-assertion for tests and mock-builders - Add @vitest/expect module augmentation for jest-dom + vitest-axe matchers (vitest 4.x moved Assertion interface to @vitest/expect) Phase 1 β Mock-builders (45 JS β TS): - Generators: typed with UserResponse, Attachment, ReactionResponse, ChannelMemberResponse, MessageResponse, PollResponse from stream-chat - API mocks: typed with StreamChat, MockedApiResponse interface - Event dispatchers: typed with StreamChat, Event types - Browser mocks: typed class properties and method signatures - Root files (index, utils, translator): typed with StreamChat, UserResponse Phase 2 Tier 1 β Simple tests (19 files): - 9 JSX β TSX: Avatar, Tooltip, EmptyStateIndicator, SafeAnchor, LoadingChannel, LoadingIndicator, LoadingChannels, InfiniteScroll, LoadMorePaginator - 10 JS β TS: DialogsManager, BehaviorSubject, Subject, useIsMounted, NotificationTranslationBuilder, TranslationBuilder, audioSampling, Attachment/utils, AudioPlayerPool, AudioPlayerNotificationsPlugin - Fixed type errors with pragmatic as-any casts for partial mocks, private property access, and missing required props
Convert all 109 remaining test files to TypeScript extensions: - 96 .test.jsx β .test.tsx - 13 .test.js β .test.ts Add // @ts-nocheck to 108 files that have pre-existing type errors from the JS era. These can be removed incrementally as type errors are fixed per-file. 33 test files are already fully type-checked. Allow @typescript-eslint/ban-ts-comment in test files to support the incremental @ts-nocheck removal strategy. Zero .js/.jsx test files remain in the repository.
Remove @ts-nocheck and fix type errors in test files with 1-4 errors each. Fixes are pragmatic as-any casts for partial mock objects, incomplete context values, and missing required props. Files fixed: File, WithAudioPlayback, LoadMoreButton, MessageDeleted, CommandItem, EmoticonItem, UserItem, BaseImage, ChannelListUI, LoadingErrorIndicator, ThreadStart, Window, GalleryContext, GalleryUI, Image, ModalGallery, SuggestPollOptionForm. Files that needed only @ts-nocheck removal (already type-safe): GlobalModal, SendButton, CooldownTimer, useCooldownTimer, ConnectionStatus, MessageNotification, ReminderNotification, useDebouncedTypingActive, Dropdown, ThreadList. 81 files with @ts-nocheck remaining.
Fix type errors in files with 3-8 errors each using pragmatic as-any casts for partial mocks, incomplete context values, and mock function signatures. 72 files with @ts-nocheck remaining (down from 108). 69 test files now fully type-checked.
Fix type errors in files with 9-18 errors each. Same pragmatic approach: as-any casts for partial mocks, context values, private property access, and mock function signatures. 41 files with @ts-nocheck remaining (down from 108). 100 test files now fully type-checked.
Fix type errors in files with 1-30 errors each. Includes fixes for partial context values, mock function signatures, missing props, private property access, and type-incompatible arguments. 12 files with @ts-nocheck remaining (the largest/most complex tests). 129 test files now fully type-checked.
Remove @ts-nocheck from the largest/most complex test files and fix all type errors: - Channel.test.tsx (112 errors) - Card.test.tsx (123 errors) - MessageInput.test.tsx (70 errors) - Message.test.tsx (59 errors) - VirtualizedMessageListComponents.test.tsx (46 errors) - AudioRecorder.test.tsx (41 errors) - useMarkRead.test.tsx (30 errors) - ChannelList.test.tsx (29 errors) - AttachmentPreviewList.test.tsx (28 errors) - PollActions.test.tsx (28 errors) - MessageList.test.tsx (22 errors) - useUnreadMessagesNotificationVirtualized.test.tsx (10 errors) All 141 test files are now fully type-checked with zero @ts-nocheck directives remaining. Zero JS/JSX test files in the repository.
Phase 1 of as-any elimination: - Create src/mock-builders/context.ts with typed context builder helpers (mockChatContext, mockChannelStateContext, etc.) using fromPartial from @total-typescript/shoehorn - Fix generateMessage to accept Date | string for created_at/updated_at fields, eliminating ~64 date coercion casts in test files - Replace all as-any in 19 event dispatcher files with fromPartial<Event> (zero as-any remaining in mock-builders/event/) - Export context helpers from mock-builders barrel The context helpers and generateMessage fix prepare for Phase 2-6 where test files will adopt these instead of as-any casts.
Replace ~350 as-any casts across 41 test files with typed context
helper functions from mock-builders/context.ts:
- mockChatContext() replaces `<ChatProvider value={{...} as any}>`
- mockChannelStateContext() replaces ChannelStateProvider as-any
- mockChannelActionContext() replaces ChannelActionProvider as-any
- mockTranslationContextValue() replaces TranslationProvider as-any
- mockComponentContext() replaces ComponentProvider as-any
- mockMessageContext() replaces MessageProvider as-any
- mockTypingContext() replaces TypingProvider as-any
Also removes ~64 created_at date casts since generateMessage now
accepts Date | string natively (Omit-based intersection type fix).
Context helpers use Record<string, unknown> for overrides param to
accept any partial shape while returning properly typed context values
via fromPartial.
~753 as-any remaining (down from ~1,107).
- Replace 37 `new Poll({ client: {} as any })` with
`fromPartial<StreamChat>({})` across 5 Poll test files
- Replace mock return value as-any with `fromPartial()` or `undefined\!`
in Search, Thread, Notification, MediaRecorder, MessageComposer tests
- Revert importOriginal<typeof import(...)>() back to as-any due to
consistent-type-imports ESLint rule blocking dynamic import() types
- Use Record<string, unknown> for context helper overrides to handle
complex function types (TFunction, ComponentType) that fromPartial
can't deeply partial
~661 as-any remaining (down from ~1,107 original).
Replace as-any casts with proper types in Search, i18n, and Message test files: - Use vi.mocked() instead of (hook as any) for mock return values - Use fromPartial<SearchSource>() for search source mocks - Use fromPartial<i18n>() for i18next mocks - Use fromPartial<Mute>() for mute object mocks - Type function params (key: string) instead of (key: any) - Use @ts-expect-error for intentional type mismatch tests - Create event/utils.ts with ChannelOrResponse type to accept both Channel instances and ChannelResponse objects in event dispatchers Event dispatchers now accept Channel | ChannelResponse, eliminating channel-as-any casts at call sites.
Replace ~88 as-any/: any with proper types across 9 files:
- Type renderComponent/setup helper params with Record<string, any>
- Type wrapper components with { children?: React.ReactNode }
- Use fromPartial<React.BaseSyntheticEvent>() for mock events
- Use StreamChat/Channel types for client/channel variables
- Use importOriginal<Record<string, unknown>>() where ESLint allows
- Type file drop helpers, message helpers, and container params
- Add VirtuosoContext type for virtualized list context objects
- Use TranslationContextValue['t'] for translation function types
β¦r tests Replace : any on function params with proper types in 4 test files: - Channel.test.tsx: type renderComponent, initClient, runTest helpers, ActiveChannelSetter props, getMessageIds array type - ChannelListItem.test.tsx: type PreviewUIComponent props, helper functions, client/channel variables with StreamChat/Channel - ChannelList.test.tsx: type renderUI client param as StreamChat - ChannelHeader.test.tsx: type renderComponent helpers with Channel, StreamChat, ChannelHeaderProps Also remove unnecessary as-any from dispatchUserUpdatedEvent calls where the spread objects already satisfy Partial<UserResponse>.
Replace ~39 as-any/: any with proper types: - MediaRecorderController: type expectRegistersError params, use fromPartial<File>, fromPartial<BlobEvent>, LocalVoiceRecordingAttachment - AudioRecorder: use fromPartial<DOMRect>, ChannelActionContextValue - AudioPlayer: type overrides as Partial<AudioPlayerOptions>, use fromPartial<AudioPlayerPool>, fromPartial<DOMRect> - AmplitudeRecorder: use fromPartial<MediaStream>, AmplitudeRecorderConfig type - useMessageDeliveryStatus: type renderComponent with Channel/StreamChat - Thread/ThreadHeader: type function params with string types
β¦cess Replace ~107 private/protected property access patterns from (obj as any).prop to obj['prop'] which bypasses TypeScript's private/protected checks without requiring as-any casts. Bracket notation works for TS private/protected keywords because they're compile-time only. Reverted to (obj as any) for cases where bracket notation doesn't help: - window/navigator property assignments (read-only or missing) - Type-incompatible assignments (string to TFunction, etc.) - globalThis/module property manipulation ~506 as-any remaining (down from ~1,107 original, 54% reduction).
β¦ponse
Change generateMessage() return type from MessageResponse to LocalMessage,
matching what components actually consume (all context values use LocalMessage).
This eliminates as-any casts where generateMessage() results are passed
to MessageContext, ChannelStateContext, and component props that expect
LocalMessage.
Also widen event dispatcher message params to accept
MessageResponse | LocalMessage, and threadRepliesApi to accept both.
Some generateChannel({ messages: [...] }) calls now need as-any since
generateChannel's DeepPartial<ChannelAPIResponse> expects MessageResponse[].
β¦script - Delete 19 orphan .test.jsx.snap files left from JSβTS rename - Remove ban-ts-comment eslint relaxation (no more @ts-nocheck) - Add "types:tests" script for test type-checking via tsc --project tsconfig.test.json --noEmit
Enable three strict-mode flags in tsconfig.test.json that pass cleanly with zero errors: - strictBindCallApply: type-check bind/call/apply arguments - alwaysStrict: emit "use strict" in all files - useUnknownInCatchVariables: type catch variables as unknown Not yet enabled (require further fixes): - strictNullChecks: ~420 errors (possibly-undefined access) - strictFunctionTypes: ~38 errors (component prop type mismatches) - noImplicitThis: ~4 errors (this in regular functions) - noImplicitAny: ~1,200 errors (implicit any in untyped code)
- Enable noImplicitThis in tsconfig.test.json β fix 4 implicit this errors in MessageList scrollTo/scrollBy mock functions by adding explicit `this: HTMLElement` annotation - Narrow Record<string, any> to Record<string, unknown> in mock-builders where the values aren't destructured (getTestClient, context helpers, generators, dataavailable event) - Keep Record<string, any> in test helper functions where values are destructured (renderComponent params etc.) since unknown values can't be destructured without narrowing
The i18next-cli extract config scanned ./src/**/*.{tsx,ts} which now
includes test files after the JSβTS migration. This caused t('abc')
from Streami18n.test.ts to be extracted into all language JSON files
with empty values, failing validate-translations.
Fix: add \!./src/**/__tests__/** and \!./src/mock-builders/** exclusions
to the i18next extraction input, and remove the erroneously extracted
"abc" key from all 12 language files.
The ! negation patterns in input array don't work with i18next-cli. Use the ignore config option instead to exclude test files and mock-builders from translation key extraction. Also remove the abc key that was re-extracted into JSON files.
Codecov Reportβ
All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #3057 +/- ##
=======================================
Coverage 78.92% 78.92%
=======================================
Files 426 426
Lines 12064 12064
Branches 3853 3853
=======================================
Hits 9522 9522
Misses 2542 2542 β View full report in Codecov by Sentry. π New features to boost your workflow:
|
| renderComponent({ attachments: [generateStaticLocationResponse({})] }); | ||
| waitFor(() => { | ||
| expect(screen.getByTestId(testId)).toBeInTheDocument(); | ||
| expect(screen.getByTestId('geolocation-attachment')).toBeInTheDocument(); |
There was a problem hiding this comment.
I have a bad experience with hardcoding the same value everywhere. Variables are a bit easier to maintain - as there was before the testId var.
| const renderComponent = (props) => | ||
| render( | ||
| <ChannelStateProvider value={{}}> | ||
| <ChannelStateProvider value={{} as any}> |
There was a problem hiding this comment.
Maybe another iteration to reduce the count of these using shoehorn?
|
Size Change: 0 B Total Size: 769 kB βΉοΈ View Unchanged
|
Rename useLastOwnMessage.test.js and CommandChip.test.jsx that were added on master after the migration branch was created.
src/mock-builders/api/sendMessage.ts
Outdated
| * api - /channels/{type}/{id}/message | ||
| */ | ||
| export const sendMessageApi = ( | ||
| message: MessageResponse | Record<string, unknown> = {}, |
There was a problem hiding this comment.
Could it be dangerous to add Record<string, unknown>?
| * @param {*} channels Array of channel objects. | ||
| */ | ||
| export const queryChannelsApi = (channels = []) => { | ||
| export const queryChannelsApi = (channels: unknown[] = []) => { |
There was a problem hiding this comment.
Shall we use correct type for channels instead of unknown?
- Extract GEOLOCATION_TEST_ID constant instead of hardcoding testId
string (Martin's comment on Attachment.test.tsx:267)
- Replace {}\as any on ChannelStateProvider with mockChannelStateContext()
(Anton's comment on Attachment.test.tsx:59)
- Type sendMessageApi param as MessageResponse | LocalMessage instead
of Record<string, unknown> (Martin's comment on sendMessage.ts:11)
- Type queryChannelsApi param as DeepPartial<ChannelAPIResponse>[]
instead of unknown[] (Martin's comment on queryChannels.ts:8)
β¦thods - Type connectUser and mockClient params as StreamChat instead of any - Use bracket notation for internal properties (_user, connectionId, userToken) that are on the StreamChat type but not public API - Replace direct method assignments (client.muteUser = mock) with vi.spyOn(client, 'muteUser').mockImplementation() in Message.test.tsx (12 occurrences) β proper spy pattern for public methods - Type ThreadStart client variable as StreamChat
- Type mockClient mocks param with MockClientOverrides interface using StreamChat['getAppSettings'] and StreamChat['queryReactions'] - Type generateChannel return as ChannelAPIResponse, rename pinnedMessages to pinned_messages to match the SDK type - Type getOrCreateChannelApi param as ChannelAPIResponse - Type queryChannelsApi param as ChannelAPIResponse[] - Type generateAttachmentAction with Action type from stream-chat - Type generateFile/generateImageFile with Partial<File> - Type channelData as Partial<ChannelResponse> in createClientWithChannel - Type initChannelFromData params with GenerateChannelOptions - Use ChannelConfigWithInfo for getConfig mock return - Use GetDraftResponse for getDraft mock return - Fix ReadResponse.last_read to use string (toISOString) instead of Date
- Use TDateTimeParser from i18n/types in mockTranslationContextValue instead of as any cast - Use standard ResizeObserverCallback DOM type in ResizeObserverMock instead of custom (...args: any[]) => void - Use Partial<BlobEvent> for dataavailable event overrides - Use unknown instead of any for EventEmitterMock data params - Remove as any from reminder.ts channel (ChannelResponse is compatible) - Add comment explaining why context helpers use Record<string, unknown> (TFunction.$TFunctionBrand prevents Partial<ContextValue>)
| unread_messages: 0, | ||
| user, | ||
| }, | ||
| } as any, |
| client['connectUser'] = connectUser.bind(null, client) as any; | ||
| }); | ||
| vi.spyOn(client, 'connectUser').mockImplementation( | ||
| (_user) => connectUser(client, _user) as any, |
| generateMessage({ user: users[i % memberCount] as any }), | ||
| ) as any), | ||
| } as DeepPartial<ChannelAPIResponse>); | ||
| .map((_v, i) => generateMessage({ user: users[i % memberCount] })) as any), |
There was a problem hiding this comment.
Is this as any on purpose?
- Type chatClient/channel variables as StreamChat/Channel (Card, ChannelListItem)
- Type message/previousMessage/nextMessage as LocalMessage (utils.test)
- Type noGroupByUser as boolean (utils.test)
- Replace { children }: any with React.PropsWithChildren (4 files)
- Fix muteStatus mock to return proper { createdAt, expiresAt, muted }
- Fix message_text_updated_at to use toISOString() (string, not Date)
- Use LocalMessage cast for custom message types (customType, date)
Enable disallowTypeAnnotations: false for consistent-type-imports in
test files, allowing typeof import() syntax in generic positions.
Replace all importOriginal() as any and importOriginal<Record<string, any>>()
with properly typed importOriginal<typeof import('module/path')>()
across 11 test files.
Three categories fixed across 37 test files:
1. {} as any β fromPartial<Type>({}) (11 files)
Use fromPartial for empty mock objects (ChannelActionContextValue,
LocalMessage, AudioPlayer, etc.) and Record<string, any> for
destructured default params.
2. vi.spyOn().mockImplementation(fn as any) β @ts-expect-error (5 files)
Replace as-any on mock implementations with @ts-expect-error
comments that explicitly document the type mismatch between mock
and real function signatures.
3. Provider value as any β context helpers (21 files)
Replace TranslationProvider/ChatProvider/ComponentProvider/
MessageProvider/ChannelStateProvider value as-any casts with
mockTranslationContextValue(), mockChatContext(),
mockComponentContext(), mockMessageContext(),
mockChannelStateContext() from mock-builders.
~399 as-any remaining (down from ~1,107 original, 64% reduction).
Replace all 46 Record<string, any> annotations in test files with proper interfaces using SDK types: - renderComponent params typed with Partial<ChannelProps>, Partial<MessageListProps>, Channel, StreamChat, etc. - channelData typed as Partial<ChannelResponse> or GenerateChannelOptions - channelConfig typed as Partial<ChannelConfigWithInfo> - componentOverrides typed as Partial<ComponentContextValue> - channelStateOpts/channelActionOverrides typed as Partial<ContextValue> - PreviewUIComponent props typed as ChannelListItemUIProps - mockT options typed as Record<string, unknown> - channelListProps typed as Partial<ChannelListProps> Zero Record<string, any> remaining in test files.
amplitudesCount is not a prop on WaveProgressBar β the component calculates amplitude count internally from container width. The prop was silently ignored by React. Pass progress directly instead of through an as-any spread.
π― Goal
Migrate the entire test suite from JavaScript/JSX to TypeScript/TSX to improve IDE autocompletion, refactoring safety, and catch type errors at write time. Also replace ad-hoc
as anycasts with proper SDK types and typed test helpers.π Implementation details
Scope: 248 files changed, 3725 insertions, 2411 deletions
Infrastructure
@total-typescript/shoehornforfromPartial<T>()in test mockstsconfig.test.jsonwith proper include patterns, path aliases, and 4 strict flags (strictBindCallApply,noImplicitThis,alwaysStrict,useUnknownInCatchVariables)eslint.config.mjswith test-specific rule relaxations; enabledisallowTypeAnnotations: falsefortypeof import()invi.mock@vitest/expectmodule augmentation for jest-dom + vitest-axe matchers (vitest 4.x compatibility)yarn types:testsscript for test type-checkingi18next.config.ts)Mock-builders (45 JS β TS)
UserResponse,Attachment,ReactionResponse,ChannelMemberResponse,LocalMessage,PollResponse,ChannelAPIResponse, etc.)generateMessage()returnsLocalMessage(matches what components consume) and acceptsDate | stringfor date fieldsgenerateChannel()returnsChannelAPIResponse(renamedpinnedMessagesβpinned_messages)mockClienttyped withStreamChat, usesvi.spyOnfor public methodsMockClientOverridesinterface usingStreamChat['getAppSettings']andStreamChat['queryReactions']TokenManagertyped withfromPartial<TokenManager>()getOrCreateChannelApityped withChannelAPIResponse,queryChannelsApiwithChannelAPIResponse[]sendMessageApityped withMessageResponse | LocalMessagefromPartial<Event>(), acceptChannel | ChannelResponseandMessageResponse | LocalMessageTDateTimeParserfromi18n/typesused for translation mockResizeObserverCallbackDOM type,Partial<BlobEvent>,Partial<File>,Actionfrom SDKmockChatContext,mockChannelStateContext,mockTranslationContextValue, etc.)Test files (134 JS/JSX β TS/TSX)
as anyreduced from ~1,107 to ~435 (61% reduction)Record<string, any>reduced to 0 β all replaced with typed interfacesvalue as anyreplaced with context helpers (mockChatContext(), etc.)vi.spyOn(obj as any)replaced with@ts-expect-error+ proper spy{} as anyreplaced withfromPartial<Type>({})importOriginal() as anyreplaced withimportOriginal<typeof import('...')>()obj['prop']) instead of(obj as any).proprenderComponentparams typed with per-file interfaces using SDK typesStreamChat,Channel,LocalMessageinstead ofany{ children }: anyreplaced withReact.PropsWithChildrenamplitudesCountprop from WaveProgressBar tests.test.jsx.snapsnapshot filesRemaining
as any(~435, intentionally kept)fromPartialused where possible$TFunctionBrand)LocalMessage[]βMessageResponse[]generateChannelcallswindow/navigator/globalThis@ts-expect-error(explicit).next(value)on state/subjectsπ¨ UI Changes
No UI changes β test infrastructure only.