Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
39 changes: 11 additions & 28 deletions tests/ui/WorkspaceCategoriesTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,36 +24,17 @@ import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import type * as MockReanimatedModalModule from '../utils/mockReanimatedModal';

import createMock from '../utils/createMock';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

jest.mock('@src/components/ConfirmedRoute.tsx');

// ReanimatedModal calls onModalHide via the native Modal's onDismiss callback,
// which doesn't fire in tests because animations are disabled.
// This mock simulates that behavior synchronously so that the showConfirmModal
// promise resolves correctly in tests.
jest.mock('@components/Modal/ReanimatedModal', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {useEffect, useRef}: {useEffect: typeof React.useEffect; useRef: typeof React.useRef} = require('react');

return function MockReanimatedModal({isVisible, onModalHide, children}: {isVisible: boolean; onModalHide?: () => void; children: React.ReactNode}) {
const wasVisible = useRef<boolean>(isVisible);

useEffect(() => {
if (wasVisible.current && !isVisible) {
onModalHide?.();
}
wasVisible.current = isVisible;
}, [isVisible, onModalHide]);

if (!isVisible) {
return null;
}

return children as React.ReactElement;
};
const {default: MockReanimatedModal} = jest.requireActual<typeof MockReanimatedModalModule>('../utils/mockReanimatedModal');
return MockReanimatedModal;
});

TestHelper.setupGlobalFetchMock();
Expand Down Expand Up @@ -95,10 +76,12 @@ describe('WorkspaceCategories', () => {
await act(async () => {
await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN);
});
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
} as ResponsiveLayoutResult);
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue(
createMock<ResponsiveLayoutResult>({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
}),
);
});

afterEach(async () => {
Expand Down
38 changes: 11 additions & 27 deletions tests/ui/WorkspaceMoreFeaturesPageTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,35 +32,17 @@ import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import type * as MockReanimatedModalModule from '../utils/mockReanimatedModal';

import createMock from '../utils/createMock';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

jest.mock('@src/components/ConfirmedRoute.tsx');

// useConfirmModal resolves only after react-native-modal calls onModalHide via the underlying
// Modal's onDismiss callback, which never fires in tests because animations are disabled. This mock
// ports the same trick used by WorkspaceCategoriesTest so that showConfirmModal promises resolve.
jest.mock('@components/Modal/ReanimatedModal', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {useEffect, useRef}: {useEffect: typeof React.useEffect; useRef: typeof React.useRef} = require('react');

return function MockReanimatedModal({isVisible, onModalHide, children}: {isVisible: boolean; onModalHide?: () => void; children: React.ReactNode}) {
const wasVisible = useRef<boolean>(isVisible);

useEffect(() => {
if (wasVisible.current && !isVisible) {
onModalHide?.();
}
wasVisible.current = isVisible;
}, [isVisible, onModalHide]);

if (!isVisible) {
return null;
}

return children as React.ReactElement;
};
const {default: MockReanimatedModal} = jest.requireActual<typeof MockReanimatedModalModule>('../utils/mockReanimatedModal');
return MockReanimatedModal;
});

jest.mock('@hooks/useIsPolicyConnectedToUberReceiptPartner', () => ({__esModule: true, default: jest.fn(() => false)}));
Expand Down Expand Up @@ -166,10 +148,12 @@ describe('WorkspaceMoreFeaturesPage', () => {
await act(async () => {
await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN);
});
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
} as ResponsiveLayoutResult);
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue(
createMock<ResponsiveLayoutResult>({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
}),
);

isSmartLimitEnabledMock.mockReturnValue(false);
getCompanyFeedsMock.mockReturnValue({});
Expand Down
21 changes: 13 additions & 8 deletions tests/ui/WorkspaceTagsTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import createMock from '../utils/createMock';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
Expand Down Expand Up @@ -84,10 +85,12 @@ describe('WorkspaceTags', () => {
await act(async () => {
await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN);
});
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: true,
shouldUseNarrowLayout: true,
} as ResponsiveLayoutResult);
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue(
createMock<ResponsiveLayoutResult>({
isSmallScreenWidth: true,
shouldUseNarrowLayout: true,
}),
);
});

afterEach(async () => {
Expand Down Expand Up @@ -136,10 +139,12 @@ describe('WorkspaceTags', () => {
});

it('should show a blocking modal when trying to disable the only enabled tag when policy has requiresTag set to true', async () => {
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
} as ResponsiveLayoutResult);
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue(
createMock<ResponsiveLayoutResult>({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
}),
);

await TestHelper.signInWithTestUser();

Expand Down
8 changes: 5 additions & 3 deletions tests/ui/WorkspaceUpgradeTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,10 @@ import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import type {Policy} from '@src/types/onyx';

import type React from 'react';
import type ReactNative from 'react-native';

import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import * as LHNTestUtils from '../utils/LHNTestUtils';
Expand All @@ -31,8 +33,8 @@ import waitForBatchedUpdates from '../utils/waitForBatchedUpdates';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

jest.mock('@components/RenderHTML', () => {
const ReactMock = require('react') as typeof React;
const {Text} = require('react-native') as {Text: React.ComponentType<{children?: React.ReactNode}>};
const ReactMock = jest.requireActual<typeof React>('react');
const {Text} = jest.requireActual<typeof ReactNative>('react-native');

return ({html}: {html: string}) => {
const plainText = html.replaceAll(/<[^>]*>/g, '');
Expand Down
34 changes: 11 additions & 23 deletions tests/ui/WorkspaceWorkflowsPayerRowTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,9 @@ import {NavigationContainer} from '@react-navigation/native';
import React from 'react';
import Onyx from 'react-native-onyx';

import type * as MockReanimatedModalModule from '../utils/mockReanimatedModal';

import createMock from '../utils/createMock';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';
Expand All @@ -44,25 +47,8 @@ jest.mock('react-native-render-html', () => {
});

jest.mock('@components/Modal/ReanimatedModal', () => {
// eslint-disable-next-line @typescript-eslint/no-unsafe-assignment
const {useEffect, useRef}: {useEffect: typeof React.useEffect; useRef: typeof React.useRef} = require('react');

return function MockReanimatedModal({isVisible, onModalHide, children}: {isVisible: boolean; onModalHide?: () => void; children: React.ReactNode}) {
const wasVisible = useRef<boolean>(isVisible);

useEffect(() => {
if (wasVisible.current && !isVisible) {
onModalHide?.();
}
wasVisible.current = isVisible;
}, [isVisible, onModalHide]);

if (!isVisible) {
return null;
}

return children as React.ReactElement;
};
const {default: MockReanimatedModal} = jest.requireActual<typeof MockReanimatedModalModule>('../utils/mockReanimatedModal');
return MockReanimatedModal;
});

TestHelper.setupGlobalFetchMock();
Expand Down Expand Up @@ -116,10 +102,12 @@ describe('WorkspaceWorkflowsPage - Payer row visibility', () => {
await act(async () => {
await Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, CONST.LOCALES.EN);
});
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
} as ResponsiveLayoutResult);
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue(
createMock<ResponsiveLayoutResult>({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
}),
);
});

afterEach(async () => {
Expand Down
5 changes: 3 additions & 2 deletions tests/ui/YearListItemHeaderTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import React from 'react';
import Onyx from 'react-native-onyx';

import createMock from '../utils/createMock';
import MockSearchContextProvider from '../utils/MockSearchContextProvider';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

Expand All @@ -24,7 +25,7 @@ jest.mock('@libs/Navigation/Navigation');

// Mock useResponsiveLayout to control screen size in tests
jest.mock('@hooks/useResponsiveLayout', () => jest.fn());
const mockedUseResponsiveLayout = useResponsiveLayout as jest.MockedFunction<typeof useResponsiveLayout>;
const mockedUseResponsiveLayout = jest.mocked(useResponsiveLayout);

// Mock search context with all required SearchContextStateValue and SearchContextActionsValue fields
const mockSearchStateContext = {
Expand All @@ -46,7 +47,7 @@ const mockSearchStateContext = {
shouldShowFiltersBarLoading: false,
shouldUseLiveData: false,
currentSimilarSearchHash: -1,
suggestedSearches: {} as SearchStateContextValue['suggestedSearches'],
suggestedSearches: createMock<SearchStateContextValue['suggestedSearches']>({}),
sortedReportIDs: [],
hasSelectedTransactions: false,
} satisfies SearchStateContextValue;
Expand Down
4 changes: 2 additions & 2 deletions tests/unit/AvatarUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ jest.mock('@src/libs/fileDownload/FileUtils');
jest.mock('@src/libs/fileDownload/getImageResolution');

describe('AvatarUtils', () => {
const mockFileUtils = FileUtils as jest.Mocked<typeof FileUtils>;
const mockGetImageResolution = getImageResolution as jest.Mocked<typeof getImageResolution>;
const mockFileUtils = jest.mocked(FileUtils);
const mockGetImageResolution = jest.mocked(getImageResolution);

beforeEach(() => {
jest.clearAllMocks();
Expand Down
24 changes: 17 additions & 7 deletions tests/unit/BulletListRendererTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,24 @@ import RenderHTML from '@components/RenderHTML';

import CONST from '@src/CONST';

import type {CustomRendererProps, TBlock} from 'react-native-render-html';
import type {CustomRendererProps, TBlock, TText} from 'react-native-render-html';

import React from 'react';

import createMock from '../utils/createMock';

jest.mock('@hooks/useWindowDimensions', () => () => ({windowWidth: 400}));
jest.mock('@hooks/useHasTextAncestor', () => () => false);

// Capture the html string ultimately passed to react-native-render-html so we can
// assert the orphaned <br/> stripping happens before the library sees the HTML.
const capturedSource: {html?: string} = {};

function mockGetFirstTextContent(tnode?: TBlock): string {
const firstChild = tnode?.children.at(0);
return firstChild?.type === 'text' ? firstChild.data : '';
}

jest.mock('react-native-render-html', () => {
const ReactModule = jest.requireActual<typeof React>('react');
const {View: MockView, Text: MockText} = jest.requireActual<{View: React.ComponentType; Text: React.ComponentType}>('react-native');
Expand All @@ -27,8 +35,8 @@ jest.mock('react-native-render-html', () => {
capturedSource.html = source?.html;
return ReactModule.createElement(MockView);
},
TNodeChildrenRenderer: ({tnode}: {tnode?: {mockText?: string}}) => ReactModule.createElement(MockText, null, tnode?.mockText ?? ''),
TNodeRenderer: ({tnode}: {tnode?: {mockText?: string}}) => ReactModule.createElement(MockText, null, tnode?.mockText ?? ''),
TNodeChildrenRenderer: ({tnode}: {tnode?: TBlock}) => ReactModule.createElement(MockText, null, mockGetFirstTextContent(tnode)),
TNodeRenderer: ({tnode}: {tnode?: TBlock}) => ReactModule.createElement(MockText, null, mockGetFirstTextContent(tnode)),
};
});

Expand All @@ -39,11 +47,13 @@ jest.mock('@libs/Parser', () => ({
default: {replace: (html: string) => html},
}));

const buildTNode = (text = '') => ({mockText: text}) as unknown as CustomRendererProps<TBlock>['tnode'];
const buildTextTNode = (text: string) => createMock<TText>({type: 'text', data: text});
const buildTNode = (text = '') => createMock<CustomRendererProps<TBlock>['tnode']>({type: 'block', children: [buildTextTNode(text)]});
const buildULTNode = (children: Array<{tagName: string; text: string}>) =>
({
children: children.map((child) => ({tagName: child.tagName, mockText: child.text})),
}) as unknown as CustomRendererProps<TBlock>['tnode'];
createMock<CustomRendererProps<TBlock>['tnode']>({
type: 'block',
children: children.map((child) => createMock<TBlock>({type: 'block', tagName: child.tagName, children: [buildTextTNode(child.text)]})),
});

describe('Bullet list rendering', () => {
beforeEach(() => {
Expand Down
24 changes: 4 additions & 20 deletions tests/unit/ButtonStyleUtilsTest.ts
Original file line number Diff line number Diff line change
@@ -1,27 +1,11 @@
import CONST from '@src/CONST';
import type {ThemeStyles} from '@src/styles';
import type {ThemeColors} from '@src/styles/theme/types';
import createThemeStyles from '@src/styles';
import {defaultTheme} from '@src/styles/theme';
import createStyleUtils from '@src/styles/utils';
import variables from '@src/styles/variables';

const mockTheme = {} as ThemeColors;

const mockStyles = {
buttonExtraSmall: {height: 28},
buttonSmall: {height: 32},
buttonMedium: {height: 40},
buttonLarge: {height: 48},
ph0: {paddingHorizontal: 0},
pl2: {paddingLeft: 8},
pr2: {paddingRight: 8},
pl3: {paddingLeft: 12},
pr3: {paddingRight: 12},
pl4: {paddingLeft: 16},
pr4: {paddingRight: 16},
buttonSuccess: {backgroundColor: 'green'},
buttonDanger: {backgroundColor: 'red'},
buttonOpacityDisabled: {opacity: 0.5},
} as unknown as ThemeStyles;
const mockTheme = defaultTheme;
const mockStyles = createThemeStyles(defaultTheme);

const {getButtonSizeStyle, getButtonPaddingStyle, getButtonStyleWithIcon, getButtonVariantStyles, getReportTableColumnStyles} = createStyleUtils(mockTheme, mockStyles);

Expand Down
Loading
Loading