Skip to content

Commit

Permalink
test: added MockedDeviceContext to voip unit tests (RocketChat#33553)
Browse files Browse the repository at this point in the history
  • Loading branch information
ggazzo authored Oct 12, 2024
1 parent 00cdca7 commit b9b1c0f
Show file tree
Hide file tree
Showing 12 changed files with 126 additions and 97 deletions.
87 changes: 53 additions & 34 deletions packages/mock-providers/src/MockedAppRootBuilder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { ServerMethodName, ServerMethodParameters, ServerMethodReturn } fro
import { Emitter } from '@rocket.chat/emitter';
import languages from '@rocket.chat/i18n/dist/languages';
import type { Method, OperationParams, OperationResult, PathPattern, UrlParams } from '@rocket.chat/rest-typings';
import type { ModalContextValue, TranslationKey } from '@rocket.chat/ui-contexts';
import type { Device, ModalContextValue, TranslationKey } from '@rocket.chat/ui-contexts';
import {
AuthorizationContext,
ConnectionStatusContext,
Expand All @@ -24,6 +24,8 @@ import React, { useEffect, useReducer } from 'react';
import { I18nextProvider, initReactI18next } from 'react-i18next';
import { useSyncExternalStore } from 'use-sync-external-store/shim';

import { MockedDeviceContext } from './MockedDeviceContext';

type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
Expand Down Expand Up @@ -126,6 +128,10 @@ export class MockedAppRootBuilder {

private events = new Emitter<MockedAppRootEvents>();

private audioInputDevices: Device[] = [];

private audioOutputDevices: Device[] = [];

wrap(wrapper: (children: ReactNode) => ReactNode): this {
this.wrappers.push(wrapper);
return this;
Expand Down Expand Up @@ -338,6 +344,16 @@ export class MockedAppRootBuilder {
return this;
}

withAudioInputDevices(devices: Device[]): this {
this.audioInputDevices = devices;
return this;
}

withAudioOutputDevices(devices: Device[]): this {
this.audioOutputDevices = devices;
return this;
}

private i18n = createInstance({
// debug: true,
lng: 'en',
Expand Down Expand Up @@ -382,7 +398,7 @@ export class MockedAppRootBuilder {
},
});

const { connectionStatus, server, router, settings, user, i18n, authorization, wrappers } = this;
const { connectionStatus, server, router, settings, user, i18n, authorization, wrappers, audioInputDevices, audioOutputDevices } = this;

const reduceTranslation = (translation?: ContextType<typeof TranslationContext>): ContextType<typeof TranslationContext> => {
return {
Expand Down Expand Up @@ -457,46 +473,49 @@ export class MockedAppRootBuilder {
<AvatarUrlProvider>
<CustomSoundProvider> */}
<UserContext.Provider value={user}>
{/* <DeviceProvider>*/}
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<MockedDeviceContext
availableAudioInputDevices={audioInputDevices}
availableAudioOutputDevices={audioOutputDevices}
>
<ModalContext.Provider value={modal}>
<AuthorizationContext.Provider value={authorization}>
{/* <EmojiPickerProvider>
<OmnichannelRoomIconProvider>
<UserPresenceProvider>*/}
<ActionManagerContext.Provider
value={{
generateTriggerId: () => '',
emitInteraction: () => Promise.reject(new Error('not implemented')),
getInteractionPayloadByViewId: () => undefined,
handleServerInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
openView: () => undefined,
disposeView: () => undefined,
notifyBusy: () => undefined,
notifyIdle: () => undefined,
}}
>
{/* <VideoConfProvider>
<ActionManagerContext.Provider
value={{
generateTriggerId: () => '',
emitInteraction: () => Promise.reject(new Error('not implemented')),
getInteractionPayloadByViewId: () => undefined,
handleServerInteraction: () => undefined,
off: () => undefined,
on: () => undefined,
openView: () => undefined,
disposeView: () => undefined,
notifyBusy: () => undefined,
notifyIdle: () => undefined,
}}
>
{/* <VideoConfProvider>
<CallProvider>
<OmnichannelProvider> */}
{wrappers.reduce<ReactNode>(
(children, wrapper) => wrapper(children),
<>
{children}
{modal.currentModal.component}
</>,
)}
{/* </OmnichannelProvider>
{wrappers.reduce<ReactNode>(
(children, wrapper) => wrapper(children),
<>
{children}
{modal.currentModal.component}
</>,
)}
{/* </OmnichannelProvider>
</CallProvider>
</VideoConfProvider>*/}
</ActionManagerContext.Provider>
{/* </UserPresenceProvider>
</ActionManagerContext.Provider>
{/* </UserPresenceProvider>
</OmnichannelRoomIconProvider>
</EmojiPickerProvider>*/}
</AuthorizationContext.Provider>
</ModalContext.Provider>
{/* </DeviceProvider>*/}
</AuthorizationContext.Provider>
</ModalContext.Provider>
</MockedDeviceContext>
</UserContext.Provider>
{/* </CustomSoundProvider>
</AvatarUrlProvider>
Expand Down
21 changes: 21 additions & 0 deletions packages/mock-providers/src/MockedDeviceContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { DeviceContextValue } from '@rocket.chat/ui-contexts';
import { DeviceContext } from '@rocket.chat/ui-contexts';
import React from 'react';

const mockDeviceContextValue: DeviceContextValue = {
enabled: true,
selectedAudioOutputDevice: undefined,
selectedAudioInputDevice: undefined,
availableAudioOutputDevices: [],
availableAudioInputDevices: [],
setAudioOutputDevice: () => undefined,
setAudioInputDevice: () => undefined,
};

type MockedDeviceContextProps = Partial<DeviceContextValue> & {
children: React.ReactNode;
};

export const MockedDeviceContext = ({ children, ...props }: MockedDeviceContextProps) => {
return <DeviceContext.Provider value={{ ...mockDeviceContextValue, ...props }}>{children}</DeviceContext.Provider>;
};
1 change: 1 addition & 0 deletions packages/mock-providers/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export * from './MockedModalContext';
export * from './MockedServerContext';
export * from './MockedSettingsContext';
export * from './MockedUserContext';
export * from './MockedDeviceContext';
17 changes: 9 additions & 8 deletions packages/ui-voip/src/components/VoipPopup/VoipPopup.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,48 +18,49 @@ jest.mock('../../hooks/useVoipDialer', () => ({
}));

const mockedUseVoipSession = jest.mocked(useVoipSession);
const appRoot = mockAppRoot();

it('should properly render incoming popup', async () => {
mockedUseVoipSession.mockImplementationOnce(() => createMockVoipSession({ type: 'INCOMING' }));
render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByTestId('vc-popup-incoming')).toBeInTheDocument();
});

it('should properly render ongoing popup', async () => {
mockedUseVoipSession.mockImplementationOnce(() => createMockVoipSession({ type: 'ONGOING' }));

render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByTestId('vc-popup-ongoing')).toBeInTheDocument();
});

it('should properly render outgoing popup', async () => {
mockedUseVoipSession.mockImplementationOnce(() => createMockVoipSession({ type: 'OUTGOING' }));

render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByTestId('vc-popup-outgoing')).toBeInTheDocument();
});

it('should properly render error popup', async () => {
mockedUseVoipSession.mockImplementationOnce(() => createMockVoipSession({ type: 'ERROR' }));

render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByTestId('vc-popup-error')).toBeInTheDocument();
});

it('should properly render dialer popup', async () => {
render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByTestId('vc-popup-dialer')).toBeInTheDocument();
});

it('should prioritize session over dialer', async () => {
mockedUseVoipSession.mockImplementationOnce(() => createMockVoipSession({ type: 'INCOMING' }));

render(<VoipPopup />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopup />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.queryByTestId('vc-popup-dialer')).not.toBeInTheDocument();
expect(screen.getByTestId('vc-popup-incoming')).toBeInTheDocument();
Expand All @@ -68,12 +69,12 @@ it('should prioritize session over dialer', async () => {
const testCases = Object.values(composeStories(stories)).map((story) => [story.storyName || 'Story', story]);

test.each(testCases)(`renders %s without crashing`, async (_storyname, Story) => {
const tree = render(<Story />, { wrapper: mockAppRoot().build(), legacyRoot: true });
const tree = render(<Story />, { wrapper: appRoot.build(), legacyRoot: true });
expect(replaceReactAriaIds(tree.baseElement)).toMatchSnapshot();
});

test.each(testCases)('%s should have no a11y violations', async (_storyname, Story) => {
const { container } = render(<Story />, { wrapper: mockAppRoot().build(), legacyRoot: true });
const { container } = render(<Story />, { wrapper: appRoot.build(), legacyRoot: true });

const results = await axe(container);
expect(results).toHaveNoViolations();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ const MockVoipClient = class extends Emitter {

setSessionType(type: VoipSession['type']) {
this._sessionType = type;
setTimeout(() => this.emit('stateChanged'), 0);
}

getSession = () =>
Expand Down Expand Up @@ -47,11 +48,6 @@ const queryClient = new QueryClient({
queries: { retry: false },
mutations: { retry: false },
},
logger: {
log: console.log,
warn: console.warn,
error: () => undefined,
},
});

export default {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,42 @@ import { render, screen } from '@testing-library/react';

import VoipPopupHeader from './VoipPopupHeader';

const appRoot = mockAppRoot();

it('should render title', () => {
render(<VoipPopupHeader>voice call header title</VoipPopupHeader>, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader>voice call header title</VoipPopupHeader>, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByText('voice call header title')).toBeInTheDocument();
});

it('should not render close button when onClose is not provided', () => {
render(<VoipPopupHeader />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.queryByRole('button', { name: 'Close' })).not.toBeInTheDocument();
});

it('should render close button when onClose is provided', () => {
render(<VoipPopupHeader onClose={jest.fn()} />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader onClose={jest.fn()} />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByRole('button', { name: 'Close' })).toBeInTheDocument();
});

it('should call onClose when close button is clicked', () => {
const closeFn = jest.fn();
render(<VoipPopupHeader onClose={closeFn} />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader onClose={closeFn} />, { wrapper: appRoot.build(), legacyRoot: true });

screen.getByRole('button', { name: 'Close' }).click();
expect(closeFn).toHaveBeenCalled();
});

it('should render settings button by default', () => {
render(<VoipPopupHeader />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByRole('button', { name: /Device_settings/ })).toBeInTheDocument();
});

it('should not render settings button when hideSettings is true', () => {
render(<VoipPopupHeader hideSettings>text</VoipPopupHeader>, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipPopupHeader hideSettings>text</VoipPopupHeader>, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.queryByRole('button', { name: /Device_settings/ })).not.toBeInTheDocument();
});
Original file line number Diff line number Diff line change
Expand Up @@ -6,28 +6,31 @@ import VoipDialerView from './VoipDialerView';

const makeCall = jest.fn();
const closeDialer = jest.fn();

const appRoot = mockAppRoot();

jest.mock('../../../hooks/useVoipAPI', () => ({
useVoipAPI: jest.fn(() => ({ makeCall, closeDialer })),
}));

it('should look good', async () => {
render(<VoipDialerView />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipDialerView />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByText('New_Call')).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Device_settings/ })).toBeInTheDocument();
expect(screen.getByRole('button', { name: /Call/i })).toBeDisabled();
});

it('should only enable call button if input has value (keyboard)', async () => {
render(<VoipDialerView />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipDialerView />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByRole('button', { name: /Call/i })).toBeDisabled();
await userEvent.type(screen.getByLabelText('Phone_number'), '123');
expect(screen.getByRole('button', { name: /Call/i })).toBeEnabled();
});

it('should only enable call button if input has value (mouse)', async () => {
render(<VoipDialerView />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipDialerView />, { wrapper: appRoot.build(), legacyRoot: true });

expect(screen.getByRole('button', { name: /Call/i })).toBeDisabled();
screen.getByTestId(`dial-pad-button-1`).click();
Expand All @@ -37,7 +40,7 @@ it('should only enable call button if input has value (mouse)', async () => {
});

it('should call methods makeCall and closeDialer when call button is clicked', async () => {
render(<VoipDialerView />, { wrapper: mockAppRoot().build(), legacyRoot: true });
render(<VoipDialerView />, { wrapper: appRoot.build(), legacyRoot: true });

await userEvent.type(screen.getByLabelText('Phone_number'), '123');
screen.getByTestId(`dial-pad-button-1`).click();
Expand Down
Loading

0 comments on commit b9b1c0f

Please sign in to comment.