Skip to content

Commit e43ed04

Browse files
committed
refactor(tests): enable noImplicitAny and annotate implicit-any sites
Flips \`noImplicitAny: true\` in \`tsconfig.test.json\` (removes the override — now inherited from the base config) and fixes the ~630 resulting errors across ~20 test files. All annotations use SDK types from \`stream-chat\` where applicable; \`as unknown as T\` casts reserved for spots where the SDK type doesn't fit cleanly at the test boundary. Patterns applied: - \`let client\`, \`let channel\`, \`let chatClient\` → \`StreamChat\` / \`Channel\` / \`StreamChat\`. - \`renderComponent\` / \`Wrapper\` / \`renderMessage\` helper signatures typed via \`ComponentProps<typeof X>\`, \`Partial<ChannelProps>\`, \`ComponentOverrides\`, or \`PropsWithChildren<Partial<...>>\`. - Context-consumer arrows (\`({ fn }) => ...\`) typed with \`ChatContextValue\` / \`TranslationContextValue\`. - Translation \`obj[key]\` → \`obj[key as keyof typeof obj]\`. - Uninitialized locals typed with a definite-assigned cast (\`{} as ChatContextValue\`) to match the established pattern. - \`_fiber\` and \`offlineDb.syncManager.invokeSyncStatusListeners\` accesses handled through narrow local shim types (\`TestOfflineDb\`, \`TestSyncManager\`) rather than widening the real SDK types. - \`updatedMessage\` \`Array.find\` results narrowed with \`!\` at assertion sites. Notable scope decisions (kept out of scope): - \`Channel\`'s \`doUpdateMessageRequest\` callback signature mismatches the SDK — cast the arg to \`Awaited<ReturnType<typeof channel.messageComposer.compose>>\` at the mock site to preserve runtime behavior. - \`messageComposer.compose\` mock resolves with \`{ localMessage, message }\` (no \`options\`) — dropped the dead \`options\` key; the SDK's \`MessageComposerMiddlewareState\` doesn't include it. - Removed unused \`TestWsConnection\` type in \`offline-feature.tsx\` and the unused \`MessageInputContextValue\` / \`MessagesContextValue\` type-imports in \`optimistic-update.tsx\` to satisfy \`@typescript-eslint/no-unused-vars\`. test:typecheck: 636 → 0. Full test suite: same pre-existing SQLite-isolation flake in \`offline-support/index.test.ts\`; no regressions. \`yarn lint\` / \`yarn build\` clean. Closes #3563.
1 parent 58c782d commit e43ed04

22 files changed

Lines changed: 484 additions & 241 deletions

File tree

package/src/__tests__/offline-support/offline-feature.tsx

Lines changed: 178 additions & 107 deletions
Large diffs are not rendered by default.

package/src/__tests__/offline-support/optimistic-update.tsx

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,13 @@ import { View } from 'react-native';
44
import { act, cleanup, render, screen, waitFor } from '@testing-library/react-native';
55

66
import type {
7+
Channel as ChannelLLC,
78
ChannelAPIResponse,
89
ChannelMemberResponse,
910
LocalMessage,
1011
ReactionResponse,
12+
StreamChat,
13+
UserResponse,
1114
} from 'stream-chat';
1215
import { v4 as uuidv4 } from 'uuid';
1316

@@ -41,13 +44,40 @@ const Channel = ChannelRaw as unknown as React.ComponentType<
4144
React.ComponentProps<typeof ChannelRaw> & { initialValue?: string }
4245
>;
4346

47+
// Tests reach into internal / private StreamChat + LLC Channel APIs (sync manager, legacy
48+
// `wsConnection`, `_deleteMessage`, `_sendReaction`, `_sendMessage`). Helpers narrow at the
49+
// call sites without sprinkling `any` everywhere.
50+
type TestPendingTask = { id: number; type: string; payload: unknown };
51+
type TestSyncManager = {
52+
invokeSyncStatusListeners: (recovered: boolean) => Promise<void>;
53+
};
54+
// Intentionally not intersected with the real `StreamChat['offlineDb']` — the
55+
// real `syncManager` member is a class with `invokeSyncStatusListeners` marked
56+
// private, which conflicts with the test-only accessor. Kept as a standalone
57+
// test shim shape.
58+
type TestOfflineDb = {
59+
addPendingTask: (task: {
60+
channelId: string | undefined;
61+
channelType: string;
62+
messageId: string;
63+
payload: unknown;
64+
type: string;
65+
}) => Promise<void>;
66+
deletePendingTask: (params: { id: number }) => Promise<void>;
67+
getPendingTasks: () => Promise<TestPendingTask[]>;
68+
syncManager: TestSyncManager;
69+
};
70+
const getOfflineDb = (client: StreamChat): TestOfflineDb =>
71+
client.offlineDb as unknown as TestOfflineDb;
72+
4473
test('Workaround to allow exporting tests', () => expect(true).toBe(true));
4574

4675
export const OptimisticUpdates = () => {
4776
describe('Optimistic Updates', () => {
48-
let chatClient;
77+
let chatClient: StreamChat;
4978

50-
const getRandomInt = (lower, upper) => Math.floor(lower + Math.random() * (upper - lower + 1));
79+
const getRandomInt = (lower: number, upper: number) =>
80+
Math.floor(lower + Math.random() * (upper - lower + 1));
5181
const createChannel = () => {
5282
const allUsers = Array(20).fill(1).map(generateUser);
5383
const allMessages: LocalMessage[] = [];
@@ -63,7 +93,7 @@ export const OptimisticUpdates = () => {
6393
const begin = getRandomInt(0, allUsers.length - 2); // begin shouldn't be the end of users.length
6494
const end = getRandomInt(begin + 1, allUsers.length - 1);
6595
const usersForMembers = allUsers.slice(begin, end);
66-
const members = usersForMembers.map((user) =>
96+
const members = usersForMembers.map((user: UserResponse) =>
6797
generateMember({
6898
user,
6999
}),
@@ -78,7 +108,7 @@ export const OptimisticUpdates = () => {
78108
const end = getRandomInt(begin + 1, usersForMembers.length - 1);
79109

80110
const usersForReactions = usersForMembers.slice(begin, end);
81-
const reactions = usersForReactions.map((user) =>
111+
const reactions = usersForReactions.map((user: UserResponse) =>
82112
generateReaction({
83113
message_id: id,
84114
user,
@@ -94,7 +124,7 @@ export const OptimisticUpdates = () => {
94124
});
95125
});
96126

97-
const reads = members.map((member) => ({
127+
const reads = members.map((member: ChannelMemberResponse) => ({
98128
last_read: new Date(new Date().setDate(new Date().getDate() - getRandomInt(0, 20))),
99129
unread_messages: getRandomInt(0, messages.length),
100130
user: member.user,
@@ -136,7 +166,10 @@ export const OptimisticUpdates = () => {
136166
channels: [channelResponse] as unknown as ChannelAPIResponse[],
137167
isLatestMessagesSet: true,
138168
});
139-
chatClient.wsConnection = { isHealthy: true, onlineStatusChanged: jest.fn() };
169+
chatClient.wsConnection = {
170+
isHealthy: true,
171+
onlineStatusChanged: jest.fn(),
172+
} as unknown as StreamChat['wsConnection'];
140173
});
141174

142175
afterEach(() => {
@@ -146,11 +179,19 @@ export const OptimisticUpdates = () => {
146179
jest.clearAllMocks();
147180
});
148181

149-
let channel;
182+
let channel: ChannelLLC;
150183
// This component is used for performing effects in a component that consumes ChannelContext,
151184
// i.e. making use of the callbacks & values provided by the Channel component.
152185
// the effect is called every time channelContext changes
153-
const CallbackEffectWithContext = ({ callback, children, context }) => {
186+
const CallbackEffectWithContext = <T,>({
187+
callback,
188+
children,
189+
context,
190+
}: {
191+
callback: (ctx: T) => Promise<void> | void;
192+
children: React.ReactNode;
193+
context: React.Context<T>;
194+
}) => {
154195
const ctx = useContext(context);
155196
const [ready, setReady] = useState(false);
156197
useEffect(() => {
@@ -166,7 +207,7 @@ export const OptimisticUpdates = () => {
166207
return null;
167208
}
168209

169-
return children;
210+
return <>{children}</>;
170211
};
171212

172213
describe('delete message', () => {
@@ -297,7 +338,7 @@ export const OptimisticUpdates = () => {
297338
localMessage: newMessage,
298339
message: newMessage,
299340
options: {},
300-
});
341+
} as unknown as Awaited<ReturnType<typeof channel.messageComposer.compose>>);
301342

302343
render(
303344
<Chat client={chatClient} enableOfflineSupport>
@@ -340,7 +381,7 @@ export const OptimisticUpdates = () => {
340381
<CallbackEffectWithContext
341382
callback={async ({ sendMessage }) => {
342383
useMockedApis(chatClient, [sendMessageApi(newMessage)]);
343-
await sendMessage({ customMessageData: newMessage });
384+
await sendMessage();
344385
}}
345386
context={MessageInputContext}
346387
>
@@ -430,8 +471,8 @@ export const OptimisticUpdates = () => {
430471
<Channel
431472
channel={channel}
432473
doUpdateMessageRequest={
433-
(async (_channelId, localMessage, options) => {
434-
await chatClient.offlineDb.addPendingTask({
474+
(async (_channelId: string, localMessage: LocalMessage, options: unknown) => {
475+
await getOfflineDb(chatClient).addPendingTask({
435476
channelId: channel.id,
436477
channelType: channel.type,
437478
messageId: message.id,
@@ -475,8 +516,8 @@ export const OptimisticUpdates = () => {
475516
const dbMessages = await BetterSqlite.selectFromTable('messages');
476517
const dbMessage = dbMessages.find((row) => row.id === message.id);
477518

478-
expect(updatedMessage.text).toBe(editedText);
479-
expect(updatedMessage.message_text_updated_at).toBeTruthy();
519+
expect(updatedMessage!.text).toBe(editedText);
520+
expect(updatedMessage!.message_text_updated_at).toBeTruthy();
480521
expect(pendingTasksRows).toHaveLength(1);
481522
expect(pendingTasksRows[0].type).toBe('update-message');
482523
expect(dbMessage!.text).toBe(editedText);
@@ -527,7 +568,7 @@ export const OptimisticUpdates = () => {
527568
const dbMessages = await BetterSqlite.selectFromTable('messages');
528569
const dbMessage = dbMessages.find((row) => row.id === message.id);
529570

530-
expect(updatedMessage.text).toBe(editedText);
571+
expect(updatedMessage!.text).toBe(editedText);
531572
expect(pendingTasksRows).toHaveLength(0);
532573
expect(dbMessage!.text).toBe(editedText);
533574
});
@@ -638,8 +679,8 @@ export const OptimisticUpdates = () => {
638679
const dbMessage = dbMessages.find((row) => row.id === message.id);
639680
const storedAttachments = JSON.parse(dbMessage!.attachments as string);
640681

641-
expect(updatedMessage.text).toBe(editedText);
642-
expect(updatedMessage.attachments[0].asset_url).toBe(localUri);
682+
expect(updatedMessage!.text).toBe(editedText);
683+
expect(updatedMessage!.attachments![0].asset_url).toBe(localUri);
643684
expect(pendingTasksRows).toHaveLength(0);
644685
expect(dbMessage!.text).toBe(editedText);
645686
expect(storedAttachments[0].asset_url).toBe(localUri);
@@ -706,7 +747,7 @@ export const OptimisticUpdates = () => {
706747
localMessage: newMessage,
707748
message: newMessage,
708749
options: {},
709-
});
750+
} as unknown as Awaited<ReturnType<typeof channel.messageComposer.compose>>);
710751

711752
// initialValue is needed as a prop to trick the message input ctx into thinking
712753
// we are sending a message.
@@ -760,11 +801,11 @@ export const OptimisticUpdates = () => {
760801
user_id: chatClient.userID,
761802
});
762803

763-
jest.spyOn(channel.messageComposer, 'compose').mockResolvedValue({
764-
localMessage,
765-
message: localMessage,
766-
options: {},
767-
});
804+
jest
805+
.spyOn(channel.messageComposer, 'compose')
806+
.mockResolvedValue({ localMessage, message: localMessage } as unknown as Awaited<
807+
ReturnType<typeof channel.messageComposer.compose>
808+
>);
768809

769810
render(
770811
<Chat client={chatClient} enableOfflineSupport>
@@ -783,23 +824,25 @@ export const OptimisticUpdates = () => {
783824
);
784825
await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
785826

786-
let pendingTask;
827+
let pendingTask: TestPendingTask | undefined;
787828
await waitFor(async () => {
788-
const pendingTasks = await chatClient.offlineDb.getPendingTasks();
829+
const pendingTasks = await getOfflineDb(chatClient).getPendingTasks();
789830
expect(pendingTasks).toHaveLength(1);
790831
pendingTask = pendingTasks[0];
791832
});
792833

793834
expect(channel.state.messages.some((message) => message.id === localMessage.id)).toBe(true);
794835

795-
jest.spyOn(channel, 'watch').mockResolvedValue({});
836+
jest
837+
.spyOn(channel, 'watch')
838+
.mockResolvedValue({} as Awaited<ReturnType<typeof channel.watch>>);
796839

797840
channel.state.removeMessage(localMessage);
798841
channel.state.addMessageSorted(serverMessage, true);
799-
await chatClient.offlineDb.deletePendingTask({ id: pendingTask.id });
842+
await getOfflineDb(chatClient).deletePendingTask({ id: pendingTask!.id });
800843

801844
await act(async () => {
802-
await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true);
845+
await getOfflineDb(chatClient).syncManager.invokeSyncStatusListeners(true);
803846
});
804847

805848
await waitFor(() => {
@@ -821,11 +864,11 @@ export const OptimisticUpdates = () => {
821864
user_id: chatClient.userID,
822865
});
823866

824-
jest.spyOn(channel.messageComposer, 'compose').mockResolvedValue({
825-
localMessage,
826-
message: localMessage,
827-
options: {},
828-
});
867+
jest
868+
.spyOn(channel.messageComposer, 'compose')
869+
.mockResolvedValue({ localMessage, message: localMessage } as unknown as Awaited<
870+
ReturnType<typeof channel.messageComposer.compose>
871+
>);
829872

830873
render(
831874
<Chat client={chatClient} enableOfflineSupport>
@@ -844,20 +887,22 @@ export const OptimisticUpdates = () => {
844887
);
845888
await waitFor(() => expect(screen.getByTestId('children')).toBeTruthy());
846889

847-
let pendingTask;
890+
let pendingTask: TestPendingTask | undefined;
848891
await waitFor(async () => {
849-
const pendingTasks = await chatClient.offlineDb.getPendingTasks();
892+
const pendingTasks = await getOfflineDb(chatClient).getPendingTasks();
850893
expect(pendingTasks).toHaveLength(1);
851894
pendingTask = pendingTasks[0];
852895
});
853896

854-
jest.spyOn(channel, 'watch').mockResolvedValue({});
897+
jest
898+
.spyOn(channel, 'watch')
899+
.mockResolvedValue({} as Awaited<ReturnType<typeof channel.watch>>);
855900

856901
channel.state.removeMessage(localMessage);
857-
await chatClient.offlineDb.deletePendingTask({ id: pendingTask.id });
902+
await getOfflineDb(chatClient).deletePendingTask({ id: pendingTask!.id });
858903

859904
await act(async () => {
860-
await chatClient.offlineDb.syncManager.invokeSyncStatusListeners(true);
905+
await getOfflineDb(chatClient).syncManager.invokeSyncStatusListeners(true);
861906
});
862907

863908
await waitFor(() => {

package/src/components/Attachment/__tests__/Giphy.test.tsx

Lines changed: 28 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -195,14 +195,23 @@ describe('Giphy', () => {
195195
attachment.giphy = giphy;
196196
render(getAttachmentComponent({ attachment, giphyVersion: 'fixed_height' }));
197197
await waitFor(() => {
198-
const checkImageProps = (imageProps, specificSizedGiphyData) => {
199-
let imageStyle = imageProps.style;
198+
const checkImageProps = (
199+
imageProps: Record<string, unknown>,
200+
specificSizedGiphyData: { height: string; url: string; width: string },
201+
) => {
202+
let imageStyle = imageProps.style as
203+
| Record<string, unknown>
204+
| Array<Record<string, unknown>>;
200205
if (Array.isArray(imageStyle)) {
201206
imageStyle = Object.assign({}, ...imageStyle);
202207
}
203-
expect(imageStyle.height).toBe(parseFloat(specificSizedGiphyData.height));
204-
expect(imageStyle.width).toBe(parseFloat(specificSizedGiphyData.width));
205-
expect(imageProps.source.uri).toBe(specificSizedGiphyData.url);
208+
expect((imageStyle as Record<string, unknown>).height).toBe(
209+
parseFloat(specificSizedGiphyData.height),
210+
);
211+
expect((imageStyle as Record<string, unknown>).width).toBe(
212+
parseFloat(specificSizedGiphyData.width),
213+
);
214+
expect((imageProps.source as { uri: string }).uri).toBe(specificSizedGiphyData.url);
206215
};
207216
checkImageProps(
208217
screen.getByLabelText('Giphy Attachment Image').props,
@@ -211,14 +220,23 @@ describe('Giphy', () => {
211220
});
212221
render(getAttachmentComponent({ attachment, giphyVersion: 'original' }));
213222
await waitFor(() => {
214-
const checkImageProps = (imageProps, specificSizedGiphyData) => {
215-
let imageStyle = imageProps.style;
223+
const checkImageProps = (
224+
imageProps: Record<string, unknown>,
225+
specificSizedGiphyData: { height: string; url: string; width: string },
226+
) => {
227+
let imageStyle = imageProps.style as
228+
| Record<string, unknown>
229+
| Array<Record<string, unknown>>;
216230
if (Array.isArray(imageStyle)) {
217231
imageStyle = Object.assign({}, ...imageStyle);
218232
}
219-
expect(imageStyle.height).toBe(parseFloat(specificSizedGiphyData.height));
220-
expect(imageStyle.width).toBe(parseFloat(specificSizedGiphyData.width));
221-
expect(imageProps.source.uri).toBe(specificSizedGiphyData.url);
233+
expect((imageStyle as Record<string, unknown>).height).toBe(
234+
parseFloat(specificSizedGiphyData.height),
235+
);
236+
expect((imageStyle as Record<string, unknown>).width).toBe(
237+
parseFloat(specificSizedGiphyData.width),
238+
);
239+
expect((imageProps.source as { uri: string }).uri).toBe(specificSizedGiphyData.url);
222240
};
223241
checkImageProps(
224242
screen.getByLabelText('Giphy Attachment Image').props,

package/src/components/AutoCompleteInput/__tests__/AutoCompleteInput.test.tsx

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,28 @@
11
import React from 'react';
22

33
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react-native';
4+
import type { Channel as ChannelType, StreamChat } from 'stream-chat';
45

56
import { OverlayProvider } from '../../../contexts';
67
import { initiateClientWithChannels } from '../../../mock-builders/api/initiateClientWithChannels';
8+
import type { ChannelProps } from '../../Channel/Channel';
79
import { Channel } from '../../Channel/Channel';
810
import { Chat } from '../../Chat/Chat';
911
import { AutoCompleteInput } from '../AutoCompleteInput';
1012

11-
const renderComponent = ({ channelProps, client, props }) => {
13+
const renderComponent = ({
14+
channelProps,
15+
client,
16+
props,
17+
}: {
18+
channelProps: Partial<ChannelProps>;
19+
client: StreamChat;
20+
props: React.ComponentProps<typeof AutoCompleteInput>;
21+
}) => {
1222
return render(
1323
<OverlayProvider>
1424
<Chat client={client}>
15-
<Channel {...channelProps}>
25+
<Channel {...(channelProps as ChannelProps)}>
1626
<AutoCompleteInput {...props} />
1727
</Channel>
1828
</Chat>
@@ -21,8 +31,8 @@ const renderComponent = ({ channelProps, client, props }) => {
2131
};
2232

2333
describe('AutoCompleteInput', () => {
24-
let client;
25-
let channel;
34+
let client: StreamChat;
35+
let channel: ChannelType;
2636

2737
beforeEach(async () => {
2838
const { client: chatClient, channels } = await initiateClientWithChannels();
@@ -70,7 +80,7 @@ describe('AutoCompleteInput', () => {
7080
it('should have the maxLength same as the one on the config of channel', async () => {
7181
jest.spyOn(channel, 'getConfig').mockReturnValue({
7282
max_message_length: 10,
73-
});
83+
} as unknown as ReturnType<typeof channel.getConfig>);
7484
const channelProps = { channel };
7585
const props = {};
7686

0 commit comments

Comments
 (0)