Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
47bd84a
chore: migrate mock-builders and simple tests to TypeScript
oliverlaz Mar 24, 2026
49402e0
chore: rename all remaining test files from JS/JSX to TS/TSX
oliverlaz Mar 24, 2026
fc772c8
chore: remove @ts-nocheck from 27 test files and fix type errors
oliverlaz Mar 24, 2026
c072f21
chore: remove @ts-nocheck from 29 more test files and fix type errors
oliverlaz Mar 24, 2026
eee17c5
chore: remove @ts-nocheck from 31 more test files and fix type errors
oliverlaz Mar 24, 2026
6dadd88
chore: remove @ts-nocheck from 29 more test files and fix type errors
oliverlaz Mar 24, 2026
0a8eaf0
chore: remove @ts-nocheck from final 12 test files and fix type errors
oliverlaz Mar 24, 2026
c17bd40
chore: add typed test helpers and eliminate as-any from mock-builders
oliverlaz Mar 24, 2026
a2b95d9
chore: replace provider as-any casts with typed context helpers
oliverlaz Mar 24, 2026
f1bbe90
chore: replace Poll client, mock return value, and misc as-any casts
oliverlaz Mar 24, 2026
8f7a5be
chore: improve test typing with fromPartial, vi.mocked, and typed params
oliverlaz Mar 24, 2026
1bac9ab
chore: type function params and replace as-any in Message/Composer tests
oliverlaz Mar 24, 2026
db426ab
chore: type function params in Channel, ChannelList, and ChannelHeade…
oliverlaz Mar 24, 2026
ebb9d33
chore: type MediaRecorder, AudioPlayback, and Thread test files
oliverlaz Mar 24, 2026
bb64ed8
chore: replace (obj as any).prop with bracket notation for private ac…
oliverlaz Mar 25, 2026
ad7a06b
chore: return LocalMessage from generateMessage instead of MessageRes…
oliverlaz Mar 25, 2026
3444529
chore: clean up orphan snapshots, eslint config, and add types:tests …
oliverlaz Mar 25, 2026
bcd2eeb
chore: enable strict TypeScript flags for test config
oliverlaz Mar 25, 2026
919a350
chore: enable noImplicitThis and narrow Record types in mock-builders
oliverlaz Mar 25, 2026
1f6b68d
chore: exclude test files from i18next translation extraction
oliverlaz Mar 25, 2026
65d1e96
fix: use ignore config instead of negation patterns for i18next-cli
oliverlaz Mar 25, 2026
b9491f8
Merge branch 'master' into chore/test-typescript-migration
oliverlaz Mar 25, 2026
5e1ed59
chore: rename new test files from JS/JSX to TS/TSX
oliverlaz Mar 25, 2026
bbbdbd4
chore: address PR review comments
oliverlaz Mar 25, 2026
fd74c18
chore: type mockClient with StreamChat and use vi.spyOn for public me…
oliverlaz Mar 25, 2026
1bb124b
chore: use stream-chat types in mock-builders instead of Record/unknown
oliverlaz Mar 25, 2026
88b9d90
chore: use TDateTimeParser, BlobEvent, ResizeObserverCallback types
oliverlaz Mar 25, 2026
4654433
chore: replace variable any annotations with SDK types in tests
oliverlaz Mar 25, 2026
b59f311
chore: type importOriginal with typeof import() in vi.mock calls
oliverlaz Mar 25, 2026
7d4e7fd
chore: replace {} as any, vi.spyOn as any, and provider as any casts
oliverlaz Mar 25, 2026
c0e378f
chore: replace Record<string, any> with typed interfaces in test helpers
oliverlaz Mar 25, 2026
45871e8
chore: remove no-op amplitudesCount prop from WaveProgressBar tests
oliverlaz Mar 25, 2026
a5dbc92
Merge branch 'master' into chore/test-typescript-migration
oliverlaz Mar 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -78,4 +78,4 @@ coverage.out
# stream-chat-css/docusaurus files
docusaurus/docs/React/theming
docusaurus/docs/React/assets/stream-chat-css*
shared
sharedtsconfig.test.tsbuildinfo
8 changes: 7 additions & 1 deletion eslint.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ export default tseslint.config(
},
{
name: 'vitest',
files: ['src/**/__tests__/**'],
files: ['src/**/__tests__/**', 'src/mock-builders/**'],
plugins: { vitest: vitestPlugin },
languageOptions: {
globals: vitestPlugin.environments.env.globals,
Expand All @@ -130,6 +130,12 @@ export default tseslint.config(
'vitest/no-hooks': 'off',
'vitest/prefer-spy-on': 'warn',
'@typescript-eslint/no-empty-function': 'off', // explicitly disable for tests
'@typescript-eslint/no-explicit-any': 'off', // test mocks frequently need any
'@typescript-eslint/no-non-null-assertion': 'off', // DOM queries in tests commonly use !
'@typescript-eslint/consistent-type-imports': [
'error',
{ disallowTypeAnnotations: false },
], // allow typeof import() in vi.mock importOriginal
},
},
);
1 change: 1 addition & 0 deletions i18next.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export default defineConfig({
defaultNS: false,
extractFromComments: false,
functions: ['t', '*.t'],
ignore: ['./src/**/__tests__/**', './src/mock-builders/**'],
input: ['./src/**/*.{tsx,ts}'],
keySeparator: false,
nsSeparator: false,
Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,7 @@
"@testing-library/dom": "^10.4.0",
"@testing-library/jest-dom": "^6.6.3",
"@testing-library/react": "^16.2.0",
"@total-typescript/shoehorn": "^0.1.2",
"@types/hast": "^2.3.4",
"@types/jsdom": "^21.1.5",
"@types/linkifyjs": "^2.1.7",
Expand Down Expand Up @@ -203,6 +204,7 @@
"test": "vitest run",
"test:watch": "vitest",
"types": "tsc --emitDeclarationOnly false --noEmit",
"types:tests": "tsc --project tsconfig.test.json --noEmit",
"validate-translations": "node scripts/validate-translations.js",
"validate-cjs": "concurrently 'node scripts/validate-cjs-node-bundle.cjs' 'node scripts/validate-cjs-browser-bundle.cjs'",
"semantic-release": "semantic-release",
Expand Down
9 changes: 9 additions & 0 deletions src/@types/vitest-axe.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { TestingLibraryMatchers } from '@testing-library/jest-dom/matchers';

declare module '@vitest/expect' {
// eslint-disable-next-line @typescript-eslint/no-empty-object-type
interface Assertion<T>
extends TestingLibraryMatchers<typeof expect.stringContaining, T> {
toHaveNoViolations(): void;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,10 @@ import {

import { Attachment } from '../Attachment';
import { SUPPORTED_VIDEO_FORMATS } from '../utils';
import { generateScrapedVideoAttachment } from '../../../mock-builders';
import {
generateScrapedVideoAttachment,
mockChannelStateContext,
} from '../../../mock-builders';
import { ChannelStateProvider } from '../../../context';

const UNSUPPORTED_ATTACHMENT_TEST_ID = 'attachment-unsupported';
Expand All @@ -33,8 +36,9 @@ const ModalGallery = (props) => (
<div data-testid='gallery-attachment'>{props.customTestId}</div>
);
const Giphy = (props) => <div data-testid='giphy-attachment'>{props.customTestId}</div>;
const GEOLOCATION_TEST_ID = 'geolocation-attachment';
const Geolocation = (props) => (
<div data-testid={'geolocation-attachment'}>{props.customTestId}</div>
<div data-testid={GEOLOCATION_TEST_ID}>{props.customTestId}</div>
);

const ATTACHMENTS = {
Expand All @@ -56,7 +60,7 @@ const ATTACHMENTS = {

const renderComponent = (props) =>
render(
<ChannelStateProvider value={{}}>
<ChannelStateProvider value={mockChannelStateContext()}>
<Attachment
AttachmentActions={AttachmentActions}
Audio={Audio}
Expand Down Expand Up @@ -258,13 +262,13 @@ describe('attachment', () => {
});

it('renders shared location with Geolocation attachment', () => {
renderComponent({ attachments: [generateLiveLocationResponse()] });
renderComponent({ attachments: [generateLiveLocationResponse({})] });
waitFor(() => {
expect(screen.getByTestId(testId)).toBeInTheDocument();
expect(screen.getByTestId(GEOLOCATION_TEST_ID)).toBeInTheDocument();
});
renderComponent({ attachments: [generateStaticLocationResponse()] });
renderComponent({ attachments: [generateStaticLocationResponse({})] });
waitFor(() => {
expect(screen.getByTestId(testId)).toBeInTheDocument();
expect(screen.getByTestId(GEOLOCATION_TEST_ID)).toBeInTheDocument();
});
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ import React from 'react';
import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react';

import { Audio } from '../Audio';
import { generateAudioAttachment, generateMessage } from '../../../mock-builders';
import {
generateAudioAttachment,
generateMessage,
mockMessageContext,
} from '../../../mock-builders';
import { prettifyFileSize } from '../../MessageComposer/hooks/utils';
import { WithAudioPlayback } from '../../AudioPlayback';
import { MessageProvider } from '../../../context';
Expand Down Expand Up @@ -33,10 +37,10 @@ vi.spyOn(window, 'Audio').mockImplementation(function AudioMock(...args) {
});

const originalConsoleError = console.error;
vi.spyOn(console, 'error').mockImplementationOnce((...errorOrTextorArg) => {
vi.spyOn(console, 'error').mockImplementationOnce((...errorOrTextorArg: any[]) => {
const msg = Array.isArray(errorOrTextorArg)
? errorOrTextorArg[0]
: (errorOrTextorArg.message ?? errorOrTextorArg);
: (errorOrTextorArg['message'] ?? errorOrTextorArg);
if (msg.match('Not implemented')) return;
originalConsoleError(...errorOrTextorArg);
});
Expand Down Expand Up @@ -79,7 +83,9 @@ describe('Audio', () => {
beforeEach(() => {
// jsdom doesn't define these, so mock them instead
// see https://developer.mozilla.org/en-US/docs/Web/API/HTMLMediaElement#Methods
vi.spyOn(HTMLMediaElement.prototype, 'play').mockImplementation(() => {});
vi.spyOn(HTMLMediaElement.prototype, 'play').mockImplementation(() =>
Promise.resolve(),
);
vi.spyOn(HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
vi.spyOn(HTMLMediaElement.prototype, 'load').mockImplementation(() => {});
});
Expand All @@ -96,7 +102,9 @@ describe('Audio', () => {
});

expect(getByText(audioAttachment.title)).toBeInTheDocument();
expect(getByText(prettifyFileSize(audioAttachment.file_size))).toBeInTheDocument();
expect(
getByText(prettifyFileSize(audioAttachment.file_size as number)),
).toBeInTheDocument();
expect(container.querySelector('img')).not.toBeInTheDocument();
});

Expand All @@ -118,7 +126,7 @@ describe('Audio', () => {
const { getByTestId } = renderComponent({ og: audioAttachment });
await clickToPlay();
vi.spyOn(HTMLDivElement.prototype, 'getBoundingClientRect').mockImplementationOnce(
() => ({ width: 120, x: 0 }),
() => ({ width: 120, x: 0 }) as any,
);

vi.spyOn(HTMLAudioElement.prototype, 'currentTime', 'set').mockImplementationOnce(
Expand Down Expand Up @@ -265,10 +273,10 @@ describe('Audio', () => {
const message = generateMessage();
render(
<WithAudioPlayback allowConcurrentPlayback>
<MessageProvider value={{ message }}>
<MessageProvider value={mockMessageContext({ message })}>
<Audio attachment={audioAttachment} />
</MessageProvider>
<MessageProvider value={{ message, threadList: true }}>
<MessageProvider value={mockMessageContext({ message, threadList: true })}>
<Audio attachment={audioAttachment} />
</MessageProvider>
</WithAudioPlayback>,
Expand All @@ -287,10 +295,10 @@ describe('Audio', () => {
const message = generateMessage();
render(
<WithAudioPlayback>
<MessageProvider value={{ message }}>
<MessageProvider value={mockMessageContext({ message })}>
<Audio attachment={audioAttachment} />
</MessageProvider>
<MessageProvider value={{ message }}>
<MessageProvider value={mockMessageContext({ message })}>
<Audio attachment={audioAttachment} />
</MessageProvider>
</WithAudioPlayback>,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,45 +3,52 @@ import { cleanup, render, waitFor } from '@testing-library/react';

import { Card } from '../LinkPreview/Card';

import { fromPartial } from '@total-typescript/shoehorn';
import { ChannelActionProvider, TranslationContext } from '../../../context';
import { ChannelStateProvider } from '../../../context/ChannelStateContext';
import { ChatProvider } from '../../../context/ChatContext';
import { ComponentProvider } from '../../../context/ComponentContext';

import type { ChannelActionContextValue } from '../../../context';
import type { Channel, StreamChat } from 'stream-chat';

import {
generateChannel,
generateGiphyAttachment,
generateMember,
generateUser,
getOrCreateChannelApi,
getTestClientWithUser,
mockTranslationContext,
mockChannelStateContext,
mockChatContext,
mockComponentContext,
mockTranslationContextValue,
useMockedApis,
} from '../../../mock-builders';
import { WithAudioPlayback } from '../../AudioPlayback';

let chatClient;
let channel;
let chatClient: StreamChat;
let channel: Channel;
const user = generateUser({ id: 'userId', name: 'username' });

vi.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation();
vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation();
vi.spyOn(window.HTMLMediaElement.prototype, 'load').mockImplementation();
const channelActionContext = {};
vi.spyOn(window.HTMLMediaElement.prototype, 'play').mockImplementation(async () => {});
vi.spyOn(window.HTMLMediaElement.prototype, 'pause').mockImplementation(() => {});
vi.spyOn(window.HTMLMediaElement.prototype, 'load').mockImplementation(() => {});
const channelActionContext = fromPartial<ChannelActionContextValue>({});

const mockedChannel = generateChannel({
members: [generateMember({ user })],
messages: [],
thread: [],
});
threads: [],
} as any);

const renderCard = ({ cardProps, chatContext, theRenderer = render }) =>
const renderCard = ({ cardProps, chatContext, theRenderer = render }: any) =>
theRenderer(
<ChatProvider value={chatContext}>
<TranslationContext.Provider value={mockTranslationContext}>
<ChatProvider value={mockChatContext(chatContext)}>
<TranslationContext.Provider value={mockTranslationContextValue()}>
<ChannelActionProvider value={channelActionContext}>
<ChannelStateProvider value={{}}>
<ComponentProvider value={{}}>
<ChannelStateProvider value={mockChannelStateContext()}>
<ComponentProvider value={mockComponentContext()}>
<WithAudioPlayback>
<Card {...cardProps} />
</WithAudioPlayback>
Expand All @@ -56,7 +63,7 @@ describe('Card', () => {
beforeAll(async () => {
chatClient = await getTestClientWithUser({ id: user.id });
useMockedApis(chatClient, [getOrCreateChannelApi(mockedChannel)]);
channel = chatClient.channel('messaging', mockedChannel.id);
channel = chatClient.channel('messaging', mockedChannel['id']);
channel.query();
});

Expand All @@ -75,7 +82,7 @@ describe('Card', () => {

const attachmentTypes = ['audio', 'image', 'video'];

const cases = attachmentTypes.reduce((acc, type) => {
const cases = attachmentTypes.reduce((acc: any, type) => {
const attachment = { ...dummyAttachment, type };
acc[type] = [
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@ import React from 'react';
import { render } from '@testing-library/react';

import { FileAttachment } from '../FileAttachment';
import { TranslationContext } from '../../../context';
import { mockTranslationContext } from '../../../mock-builders';
import { TranslationProvider } from '../../../context';
import { mockTranslationContextValue } from '../../../mock-builders';

const getComponent = ({ attachment }) => (
<TranslationContext.Provider value={mockTranslationContext}>
const getComponent = ({ attachment }: any) => (
<TranslationProvider value={mockTranslationContextValue()}>
<FileAttachment attachment={attachment} />
</TranslationContext.Provider>
</TranslationProvider>
);

const file = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import { act, render, screen } from '@testing-library/react';
import { Channel } from '../../Channel';
import { Chat } from '../../Chat';
import { Geolocation } from '../Geolocation';
import type { GeolocationProps } from '../Geolocation';
import {
generateLiveLocationResponse,
generateStaticLocationResponse,
initClientWithChannels,
} from '../../../mock-builders';
import type { Channel as ChannelType, StreamChat } from 'stream-chat';

const GeolocationMapComponent = (props) => (
<div data-props={props} data-testid='geolocation-map' />
Expand All @@ -30,7 +32,13 @@ const getGeolocationMap = () => screen.queryByTestId('geolocation-map');
const ownUser = { id: 'user-id' };
const otherUser = { id: 'other-user-id' };

const renderComponent = async ({ channel, client, props } = {}) => {
const renderComponent = async (
{ channel, client, props } = {} as {
channel?: ChannelType;
client?: StreamChat;
props?: GeolocationProps;
},
) => {
const {
channels: [defaultChannel],
client: defaultClient,
Expand All @@ -40,7 +48,7 @@ const renderComponent = async ({ channel, client, props } = {}) => {
result = render(
<Chat client={client ?? defaultClient}>
<Channel channel={channel ?? defaultChannel}>
<Geolocation {...props} />
<Geolocation {...props!} />
</Channel>
</Chat>,
);
Expand Down
Loading
Loading