diff --git a/.github/workflows/e2ePerformanceTests.yml b/.github/workflows/e2ePerformanceTests.yml index 8a47ea4bb220..fd814ad69a7c 100644 --- a/.github/workflows/e2ePerformanceTests.yml +++ b/.github/workflows/e2ePerformanceTests.yml @@ -183,7 +183,7 @@ jobs: run: npm run e2e-test-runner-build - name: Copy e2e code into zip folder - run: cp tests/e2e/dist/index.js zip/testRunner.js + run: cp tests/e2e/dist/index.js zip/testRunner.ts - name: Zip everything in the zip directory up run: zip -qr App.zip ./zip diff --git a/jest.config.js b/jest.config.js index 5b36e44c7581..13645e720c8e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -23,7 +23,7 @@ module.exports = { }, testEnvironment: 'jsdom', setupFiles: ['/jest/setup.ts', './node_modules/@react-native-google-signin/google-signin/jest/build/setup.js'], - setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.js'], + setupFilesAfterEnv: ['/jest/setupAfterEnv.ts', '/tests/perf-test/setupAfterEnv.ts'], cacheDirectory: '/.jest-cache', moduleNameMapper: { '\\.(lottie)$': '/__mocks__/fileMock.ts', diff --git a/package.json b/package.json index 62da9d177dca..58e204126829 100644 --- a/package.json +++ b/package.json @@ -52,13 +52,13 @@ "symbolicate:ios": "npx metro-symbolicate main.jsbundle.map", "symbolicate-release:ios": "scripts/release-profile.js --platform=ios", "symbolicate-release:android": "scripts/release-profile.js --platform=android", - "test:e2e": "ts-node tests/e2e/testRunner.js --config ./config.local.ts", - "test:e2e:dev": "ts-node tests/e2e/testRunner.js --config ./config.dev.js", + "test:e2e": "ts-node tests/e2e/testRunner.ts --config ./config.local.ts", + "test:e2e:dev": "ts-node tests/e2e/testRunner.ts --config ./config.dev.ts", "gh-actions-unused-styles": "./.github/scripts/findUnusedKeys.sh", "workflow-test": "./workflow_tests/scripts/runWorkflowTests.sh", "workflow-test:generate": "ts-node workflow_tests/utils/preGenerateTest.js", "setup-https": "mkcert -install && mkcert -cert-file config/webpack/certificate.pem -key-file config/webpack/key.pem dev.new.expensify.com localhost 127.0.0.1", - "e2e-test-runner-build": "ncc build tests/e2e/testRunner.js -o tests/e2e/dist/" + "e2e-test-runner-build": "ncc build tests/e2e/testRunner.ts -o tests/e2e/dist/" }, "dependencies": { "@dotlottie/react-player": "^1.6.3", diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index f76bdf2ed9a5..4c0e572cc9b2 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -1,23 +1,6 @@ import Config from '../../../tests/e2e/config'; import Routes from '../../../tests/e2e/server/routes'; -import type {NetworkCacheMap, TestConfig} from './types'; - -type TestResult = { - /** Name of the test */ - name: string; - - /** The branch where test were running */ - branch?: string; - - /** Duration in milliseconds */ - duration?: number; - - /** Optional, if set indicates that the test run failed and has no valid results. */ - error?: string; - - /** Render count */ - renderCount?: number; -}; +import type {NetworkCacheMap, TestConfig, TestResult} from './types'; type NativeCommandPayload = { text: string; diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 0964938392fb..5185a75625a3 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -26,4 +26,21 @@ type TestConfig = { [key: string]: string | {autoFocus: boolean}; }; -export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig}; +type TestResult = { + /** Name of the test */ + name: string; + + /** The branch where test were running */ + branch?: string; + + /** Duration in milliseconds */ + duration?: number; + + /** Optional, if set indicates that the test run failed and has no valid results. */ + error?: string; + + /** Render count */ + renderCount?: number; +}; + +export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig, TestResult}; diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index 21f1d620b14f..effbdd9d28fa 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -330,6 +330,14 @@ declare module 'react-native' { } interface PressableProps extends WebPressableProps {} + interface AppStateStatic { + emitCurrentTestState: (status: string) => void; + } + + interface LinkingStatic { + setInitialURL: (url: string) => void; + } + /** * Styles */ diff --git a/tests/e2e/README.md b/tests/e2e/README.md index 5f124f20e872..ea36172a52ff 100644 --- a/tests/e2e/README.md +++ b/tests/e2e/README.md @@ -117,7 +117,7 @@ components: - Orchestrates the test suite. - Runs the app with the tests on a device - Responsible for gathering and comparing results - - Located in `e2e/testRunner.js`. + - Located in `e2e/testRunner.ts`. - Test server: - A nodeJS application that starts an HTTP server. diff --git a/tests/e2e/TestSpec.yml b/tests/e2e/TestSpec.yml index 333d3af7d03d..3570bc11f3bb 100644 --- a/tests/e2e/TestSpec.yml +++ b/tests/e2e/TestSpec.yml @@ -21,7 +21,7 @@ phases: test: commands: - cd zip - - node testRunner.js -- --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk + - node testRunner.ts -- --mainAppPath app-e2eRelease.apk --deltaAppPath app-e2edeltaRelease.apk artifacts: - $WORKING_DIRECTORY diff --git a/tests/e2e/config.dev.js b/tests/e2e/config.dev.ts similarity index 77% rename from tests/e2e/config.dev.js rename to tests/e2e/config.dev.ts index 0e5d3dc01a95..cdd7bce756c8 100644 --- a/tests/e2e/config.dev.js +++ b/tests/e2e/config.dev.ts @@ -1,7 +1,9 @@ +import type {Config} from './config.local'; + const packageName = 'com.expensify.chat.dev'; const appPath = './android/app/build/outputs/apk/development/debug/app-development-debug.apk'; -export default { +const config: Config = { MAIN_APP_PACKAGE: packageName, DELTA_APP_PACKAGE: packageName, MAIN_APP_PATH: appPath, @@ -9,3 +11,5 @@ export default { RUNS: 8, BOOT_COOL_DOWN: 5 * 1000, }; + +export default config; diff --git a/tests/e2e/config.local.ts b/tests/e2e/config.local.ts index 40f7afde3985..8e90da9d3423 100644 --- a/tests/e2e/config.local.ts +++ b/tests/e2e/config.local.ts @@ -10,3 +10,4 @@ const config: Config = { }; export default config; +export type {Config}; diff --git a/tests/e2e/server/index.ts b/tests/e2e/server/index.ts index 7e7c34959655..16f23fd325cb 100644 --- a/tests/e2e/server/index.ts +++ b/tests/e2e/server/index.ts @@ -1,5 +1,5 @@ -import {createServer} from 'http'; import type {IncomingMessage, ServerResponse} from 'http'; +import {createServer} from 'http'; import type {NativeCommand, TestResult} from '@libs/E2E/client'; import type {NetworkCacheMap, TestConfig} from '@libs/E2E/types'; import config from '../config'; @@ -166,7 +166,7 @@ const createServerInstance = (): ServerInstance => { return; } - const cachedData = networkCache[appInstanceId] || {}; + const cachedData = networkCache[appInstanceId] ?? {}; res.end(JSON.stringify(cachedData)); }); diff --git a/tests/e2e/testRunner.js b/tests/e2e/testRunner.ts similarity index 82% rename from tests/e2e/testRunner.js rename to tests/e2e/testRunner.ts index 35d653a1bd79..5edc8c068229 100644 --- a/tests/e2e/testRunner.js +++ b/tests/e2e/testRunner.ts @@ -16,7 +16,7 @@ /* eslint-disable @lwc/lwc/no-async-await,no-restricted-syntax,no-await-in-loop */ import {execSync} from 'child_process'; import fs from 'fs'; -import _ from 'underscore'; +import type {TestConfig} from '@libs/E2E/types'; import compare from './compare/compare'; import defaultConfig from './config'; import createServerInstance from './server'; @@ -28,9 +28,11 @@ import * as Logger from './utils/logger'; import sleep from './utils/sleep'; import withFailTimeout from './utils/withFailTimeout'; +type Result = Record; + // VARIABLE CONFIGURATION const args = process.argv.slice(2); -const getArg = (argName) => { +const getArg = (argName: string): string | undefined => { const argIndex = args.indexOf(argName); if (argIndex === -1) { return undefined; @@ -39,13 +41,13 @@ const getArg = (argName) => { }; let config = defaultConfig; -const setConfigPath = (configPathParam) => { +const setConfigPath = (configPathParam: string | undefined) => { let configPath = configPathParam; - if (!configPath.startsWith('.')) { + if (!configPath?.startsWith('.')) { configPath = `./${configPath}`; } const customConfig = require(configPath).default; - config = _.extend(defaultConfig, customConfig); + config = Object.assign(defaultConfig, customConfig); }; if (args.includes('--config')) { @@ -54,8 +56,8 @@ if (args.includes('--config')) { } // Important: set app path only after correct config file has been loaded -const mainAppPath = getArg('--mainAppPath') || config.MAIN_APP_PATH; -const deltaAppPath = getArg('--deltaAppPath') || config.DELTA_APP_PATH; +const mainAppPath = getArg('--mainAppPath') ?? config.MAIN_APP_PATH; +const deltaAppPath = getArg('--deltaAppPath') ?? config.DELTA_APP_PATH; // Check if files exists: if (!fs.existsSync(mainAppPath)) { throw new Error(`Main app path does not exist: ${mainAppPath}`); @@ -76,8 +78,7 @@ try { } // START OF TEST CODE - -const runTests = async () => { +const runTests = async (): Promise => { Logger.info('Installing apps and reversing port'); await installApp(config.MAIN_APP_PACKAGE, mainAppPath); await installApp(config.DELTA_APP_PACKAGE, deltaAppPath); @@ -88,36 +89,38 @@ const runTests = async () => { await server.start(); // Create a dict in which we will store the run durations for all tests - const results = {}; + const results: Record = {}; // Collect results while tests are being executed server.addTestResultListener((testResult) => { - if (testResult.error != null) { + if (testResult?.error != null) { throw new Error(`Test '${testResult.name}' failed with error: ${testResult.error}`); } let result = 0; - if ('duration' in testResult) { + if (testResult?.duration !== undefined) { if (testResult.duration < 0) { return; } result = testResult.duration; } - if ('renderCount' in testResult) { + if (testResult?.renderCount !== undefined) { result = testResult.renderCount; } - Logger.log(`[LISTENER] Test '${testResult.name}' on '${testResult.branch}' measured ${result}`); + Logger.log(`[LISTENER] Test '${testResult?.name}' on '${testResult?.branch}' measured ${result}`); - if (!results[testResult.branch]) { + if (testResult?.branch && !results[testResult.branch]) { results[testResult.branch] = {}; } - results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] || []).concat(result); + if (testResult?.branch && testResult?.name) { + results[testResult.branch][testResult.name] = (results[testResult.branch][testResult.name] ?? []).concat(result); + } }); // Function to run a single test iteration - async function runTestIteration(appPackage, iterationText, launchArgs) { + async function runTestIteration(appPackage: string, iterationText: string, launchArgs: Record = {}): Promise { Logger.info(iterationText); // Making sure the app is really killed (e.g. if a prior test run crashed) @@ -128,10 +131,9 @@ const runTests = async () => { await launchApp('android', appPackage, config.ACTIVITY_PATH, launchArgs); await withFailTimeout( - new Promise((resolve) => { - const cleanup = server.addTestDoneListener(() => { + new Promise((resolve) => { + server.addTestDoneListener(() => { Logger.success(iterationText); - cleanup(); resolve(); }); }), @@ -143,9 +145,9 @@ const runTests = async () => { } // Run the tests - const tests = _.values(config.TESTS_CONFIG); + const tests = Object.keys(config.TESTS_CONFIG); for (let testIndex = 0; testIndex < tests.length; testIndex++) { - const test = _.values(config.TESTS_CONFIG)[testIndex]; + const test = Object.values(config.TESTS_CONFIG)[testIndex]; // check if we want to skip the test if (args.includes('--includes')) { @@ -164,7 +166,7 @@ const runTests = async () => { Logger.info(`Cooling down for ${config.BOOT_COOL_DOWN / 1000}s`); await sleep(config.BOOT_COOL_DOWN); - server.setTestConfig(test); + server.setTestConfig(test as TestConfig); const warmupText = `Warmup for test '${test.name}' [${testIndex + 1}/${tests.length}]`; @@ -182,7 +184,7 @@ const runTests = async () => { // We run each test multiple time to average out the results for (let testIteration = 0; testIteration < config.RUNS; testIteration++) { - const onError = (e) => { + const onError = (e: Error) => { errorCountRef.errorCount += 1; if (testIteration === 0 || errorCountRef.errorCount === errorCountRef.allowedExceptions) { Logger.error("There was an error running the test and we've reached the maximum number of allowed exceptions. Stopping the test run."); @@ -191,6 +193,7 @@ const runTests = async () => { // maximum number of allowed exceptions, we should stop the test run. throw e; } + // eslint-disable-next-line @typescript-eslint/restrict-template-expressions Logger.warn(`There was an error running the test. Continuing the test run. Error: ${e}`); }; @@ -208,7 +211,7 @@ const runTests = async () => { // Run the test on the delta app: await runTestIteration(config.DELTA_APP_PACKAGE, deltaIterationText, launchArgs); } catch (e) { - onError(e); + onError(e as Error); } } } @@ -228,7 +231,7 @@ const run = async () => { process.exit(0); } catch (e) { - Logger.info('\n\nE2E test suite failed due to error:', e, '\nPrinting full logs:\n\n'); + Logger.info('\n\nE2E test suite failed due to error:', e as string, '\nPrinting full logs:\n\n'); // Write logcat, meminfo, emulator info to file as well: execSync(`adb logcat -d > ${config.OUTPUT_DIR}/logcat.txt`); @@ -237,7 +240,7 @@ const run = async () => { execSync(`cat ${config.LOG_FILE}`); try { - execSync(`cat ~/.android/avd/${process.env.AVD_NAME || 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`); + execSync(`cat ~/.android/avd/${process.env.AVD_NAME ?? 'test'}.avd/config.ini > ${config.OUTPUT_DIR}/emulator-config.ini`); } catch (ignoredError) { // the error is ignored, as the file might not exist if the test // run wasn't started with an emulator diff --git a/tests/perf-test/setupAfterEnv.js b/tests/perf-test/setupAfterEnv.ts similarity index 100% rename from tests/perf-test/setupAfterEnv.js rename to tests/perf-test/setupAfterEnv.ts diff --git a/tests/ui/UnreadIndicatorsTest.js b/tests/ui/UnreadIndicatorsTest.tsx similarity index 85% rename from tests/ui/UnreadIndicatorsTest.js rename to tests/ui/UnreadIndicatorsTest.tsx index 9c2ff134f21a..cbfb0b66d493 100644 --- a/tests/ui/UnreadIndicatorsTest.js +++ b/tests/ui/UnreadIndicatorsTest.tsx @@ -1,26 +1,30 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type * as NativeNavigation from '@react-navigation/native'; import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native'; import {addSeconds, format, subMinutes, subSeconds} from 'date-fns'; import {utcToZonedTime} from 'date-fns-tz'; -import lodashGet from 'lodash/get'; import React from 'react'; import {AppState, DeviceEventEmitter, Linking} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; +import type Animated from 'react-native-reanimated'; +import * as CollectionUtils from '@libs/CollectionUtils'; +import DateUtils from '@libs/DateUtils'; +import * as Localize from '@libs/Localize'; +import LocalNotification from '@libs/Notification/LocalNotification'; +import * as NumberUtils from '@libs/NumberUtils'; +import * as Pusher from '@libs/Pusher/pusher'; +import PusherConnectionManager from '@libs/PusherConnectionManager'; import FontUtils from '@styles/utils/FontUtils'; -import App from '../../src/App'; -import CONFIG from '../../src/CONFIG'; -import CONST from '../../src/CONST'; -import * as AppActions from '../../src/libs/actions/App'; -import * as Report from '../../src/libs/actions/Report'; -import * as User from '../../src/libs/actions/User'; -import * as CollectionUtils from '../../src/libs/CollectionUtils'; -import DateUtils from '../../src/libs/DateUtils'; -import * as Localize from '../../src/libs/Localize'; -import LocalNotification from '../../src/libs/Notification/LocalNotification'; -import * as NumberUtils from '../../src/libs/NumberUtils'; -import * as Pusher from '../../src/libs/Pusher/pusher'; -import PusherConnectionManager from '../../src/libs/PusherConnectionManager'; -import ONYXKEYS from '../../src/ONYXKEYS'; -import appSetup from '../../src/setup'; +import * as AppActions from '@userActions/App'; +import * as Report from '@userActions/Report'; +import * as User from '@userActions/User'; +import App from '@src/App'; +import CONFIG from '@src/CONFIG'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import appSetup from '@src/setup'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; import PusherHelper from '../utils/PusherHelper'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; @@ -43,7 +47,7 @@ jest.mock('react-native/Libraries/LogBox/LogBox', () => ({ })); jest.mock('react-native-reanimated', () => ({ - ...jest.requireActual('react-native-reanimated/mock'), + ...jest.requireActual('react-native-reanimated/mock'), createAnimatedPropAdapter: jest.fn, useReducedMotion: jest.fn, })); @@ -51,7 +55,12 @@ jest.mock('react-native-reanimated', () => ({ /** * We need to keep track of the transitionEnd callback so we can trigger it in our tests */ -let transitionEndCB; +let transitionEndCB: () => void; + +type ListenerMock = { + triggerTransitionEnd: () => void; + addListener: jest.Mock; +}; /** * This is a helper function to create a mock for the addListener function of the react-navigation library. @@ -60,15 +69,15 @@ let transitionEndCB; * * P.S: This can't be moved to a utils file because Jest wants any external function to stay in the scope. * - * @returns {Object} An object with two functions: triggerTransitionEnd and addListener + * @returns An object with two functions: triggerTransitionEnd and addListener */ -const createAddListenerMock = () => { - const transitionEndListeners = []; +const createAddListenerMock = (): ListenerMock => { + const transitionEndListeners: Array<() => void> = []; const triggerTransitionEnd = () => { transitionEndListeners.forEach((transitionEndListener) => transitionEndListener()); }; - const addListener = jest.fn().mockImplementation((listener, callback) => { + const addListener: jest.Mock = jest.fn().mockImplementation((listener, callback) => { if (listener === 'transitionEnd') { transitionEndListeners.push(callback); } @@ -85,14 +94,16 @@ jest.mock('@react-navigation/native', () => { const actualNav = jest.requireActual('@react-navigation/native'); const {triggerTransitionEnd, addListener} = createAddListenerMock(); transitionEndCB = triggerTransitionEnd; - const useNavigation = () => ({ - navigate: jest.fn(), - ...actualNav.useNavigation, - getState: () => ({ - routes: [], - }), - addListener, - }); + + const useNavigation = () => + ({ + navigate: jest.fn(), + ...actualNav.useNavigation, + getState: () => ({ + routes: [], + }), + addListener, + } as typeof NativeNavigation.useNavigation); return { ...actualNav, @@ -100,7 +111,7 @@ jest.mock('@react-navigation/native', () => { getState: () => ({ routes: [], }), - }; + } as typeof NativeNavigation; }); beforeAll(() => { @@ -109,6 +120,7 @@ beforeAll(() => { // fetch() never gets called so it does not need mocking) or we might have fetch throw an error to test error handling // behavior. But here we just want to treat all API requests as a generic "success" and in the cases where we need to // simulate data arriving we will just set it into Onyx directly with Onyx.merge() or Onyx.set() etc. + // @ts-expect-error -- TODO: Remove this once TestHelper (https://github.com/Expensify/App/issues/25318) is migrated global.fetch = TestHelper.getGlobalFetchMock(); Linking.setInitialURL('https://new.expensify.com/'); @@ -125,7 +137,7 @@ beforeAll(() => { function scrollUpToRevealNewMessagesBadge() { const hintText = Localize.translateLocal('sidebarScreen.listOfChatMessages'); - fireEvent.scroll(screen.queryByLabelText(hintText), { + fireEvent.scroll(screen.getByLabelText(hintText), { nativeEvent: { contentOffset: { y: 250, @@ -144,43 +156,33 @@ function scrollUpToRevealNewMessagesBadge() { }); } -/** - * @return {Boolean} - */ -function isNewMessagesBadgeVisible() { +function isNewMessagesBadgeVisible(): boolean { const hintText = Localize.translateLocal('accessibilityHints.scrollToNewestMessages'); const badge = screen.queryByAccessibilityHint(hintText); - return Math.round(badge.props.style.transform[0].translateY) === -40; + return Math.round(badge?.props.style.transform[0].translateY) === -40; } -/** - * @return {Promise} - */ -function navigateToSidebar() { +function navigateToSidebar(): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigateToChatsList'); const reportHeaderBackButton = screen.queryByAccessibilityHint(hintText); - fireEvent(reportHeaderBackButton, 'press'); + if (reportHeaderBackButton) { + fireEvent(reportHeaderBackButton, 'press'); + } return waitForBatchedUpdates(); } -/** - * @param {Number} index - * @return {Promise} - */ -async function navigateToSidebarOption(index) { +async function navigateToSidebarOption(index: number): Promise { const hintText = Localize.translateLocal('accessibilityHints.navigatesToChat'); const optionRows = screen.queryAllByAccessibilityHint(hintText); fireEvent(optionRows[index], 'press'); await waitForBatchedUpdatesWithAct(); } -/** - * @return {Boolean} - */ -function areYouOnChatListScreen() { +function areYouOnChatListScreen(): boolean { const hintText = Localize.translateLocal('sidebarScreen.listOfChats'); const sidebarLinks = screen.queryAllByLabelText(hintText); - return !lodashGet(sidebarLinks, [0, 'props', 'accessibilityElementsHidden']); + + return !sidebarLinks?.[0]?.props?.accessibilityElementsHidden; } const REPORT_ID = '1'; @@ -190,15 +192,13 @@ const USER_B_ACCOUNT_ID = 2; const USER_B_EMAIL = 'user_b@test.com'; const USER_C_ACCOUNT_ID = 3; const USER_C_EMAIL = 'user_c@test.com'; -let reportAction3CreatedDate; -let reportAction9CreatedDate; +let reportAction3CreatedDate: string; +let reportAction9CreatedDate: string; /** * Sets up a test with a logged in user that has one unread chat from another user. Returns the test instance. - * - * @returns {Promise} */ -function signInAndGetAppWithUnreadChat() { +function signInAndGetAppWithUnreadChat(): Promise { // Render the App and sign in as a test user. render(); return waitForBatchedUpdatesWithAct() @@ -268,7 +268,7 @@ function signInAndGetAppWithUnreadChat() { }); // We manually setting the sidebar as loaded since the onLayout event does not fire in tests - AppActions.setSidebarLoaded(true); + AppActions.setSidebarLoaded(); return waitForBatchedUpdatesWithAct(); }); } @@ -286,7 +286,7 @@ describe('Unread Indicators', () => { signInAndGetAppWithUnreadChat() .then(() => { // Verify no notifications are created for these older messages - expect(LocalNotification.showCommentNotification.mock.calls).toHaveLength(0); + expect((LocalNotification.showCommentNotification as jest.Mock).mock.calls).toHaveLength(0); // Verify the sidebar links are rendered const sidebarLinksHintText = Localize.translateLocal('sidebarScreen.listOfChats'); @@ -301,12 +301,12 @@ describe('Unread Indicators', () => { // And that the text is bold const displayNameHintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameText = screen.queryByLabelText(displayNameHintText); - expect(lodashGet(displayNameText, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold); + expect(displayNameText?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold); return navigateToSidebarOption(0); }) .then(async () => { - await act(() => transitionEndCB && transitionEndCB()); + await act(() => transitionEndCB?.()); // That the report actions are visible along with the created action const welcomeMessageHintText = Localize.translateLocal('accessibilityHints.chatWelcomeMessage'); @@ -320,7 +320,7 @@ describe('Unread Indicators', () => { const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); - const reportActionID = lodashGet(unreadIndicator, [0, 'props', 'data-action-id']); + const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; expect(reportActionID).toBe('4'); // Scroll up and verify that the "New messages" badge appears scrollUpToRevealNewMessagesBadge(); @@ -331,7 +331,7 @@ describe('Unread Indicators', () => { // Navigate to the unread chat from the sidebar .then(() => navigateToSidebarOption(0)) .then(async () => { - await act(() => transitionEndCB && transitionEndCB()); + await act(() => transitionEndCB?.()); // Verify the unread indicator is present const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); @@ -413,6 +413,7 @@ describe('Unread Indicators', () => { reportActionID: commentReportActionID, }, }, + // @ts-expect-error -- it's necessary for the test shouldNotify: true, }, { @@ -439,27 +440,27 @@ describe('Unread Indicators', () => { const displayNameTexts = screen.queryAllByLabelText(displayNameHintTexts); expect(displayNameTexts).toHaveLength(2); const firstReportOption = displayNameTexts[0]; - expect(lodashGet(firstReportOption, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold); - expect(lodashGet(firstReportOption, ['props', 'children', 0])).toBe('C User'); + expect(firstReportOption?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold); + expect(firstReportOption?.props?.children?.[0]).toBe('C User'); const secondReportOption = displayNameTexts[1]; - expect(lodashGet(secondReportOption, ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold); - expect(lodashGet(secondReportOption, ['props', 'children', 0])).toBe('B User'); + expect(secondReportOption?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold); + expect(secondReportOption?.props?.children?.[0]).toBe('B User'); // Tap the new report option and navigate back to the sidebar again via the back button return navigateToSidebarOption(0); }) .then(waitForBatchedUpdates) .then(async () => { - await act(() => transitionEndCB && transitionEndCB()); + await act(() => transitionEndCB?.()); // Verify that report we navigated to appears in a "read" state while the original unread report still shows as unread const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText); expect(displayNameTexts).toHaveLength(2); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('C User'); - expect(lodashGet(displayNameTexts[1], ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold); - expect(lodashGet(displayNameTexts[1], ['props', 'children', 0])).toBe('B User'); + expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(undefined); + expect(displayNameTexts[0]?.props?.children?.[0]).toBe('C User'); + expect(displayNameTexts[1]?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold); + expect(displayNameTexts[1]?.props?.children?.[0]).toBe('B User'); })); xit('Manually marking a chat message as unread shows the new line indicator and updates the LHN', () => @@ -477,7 +478,7 @@ describe('Unread Indicators', () => { const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); - const reportActionID = lodashGet(unreadIndicator, [0, 'props', 'data-action-id']); + const reportActionID = unreadIndicator[0]?.props?.['data-action-id']; expect(reportActionID).toBe('3'); // Scroll up and verify the new messages badge appears scrollUpToRevealNewMessagesBadge(); @@ -490,8 +491,8 @@ describe('Unread Indicators', () => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText); expect(displayNameTexts).toHaveLength(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(FontUtils.fontWeight.bold); - expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('B User'); + expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(FontUtils.fontWeight.bold); + expect(displayNameTexts[0]?.props?.children?.[0]).toBe('B User'); // Navigate to the report again and back to the sidebar return navigateToSidebarOption(0); @@ -502,8 +503,8 @@ describe('Unread Indicators', () => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNameTexts = screen.queryAllByLabelText(hintText); expect(displayNameTexts).toHaveLength(1); - expect(lodashGet(displayNameTexts[0], ['props', 'style', 'fontWeight'])).toBe(undefined); - expect(lodashGet(displayNameTexts[0], ['props', 'children', 0])).toBe('B User'); + expect(displayNameTexts[0]?.props?.style?.fontWeight).toBe(undefined); + expect(displayNameTexts[0]?.props?.children?.[0]).toBe('B User'); // Navigate to the report again and verify the new line indicator is missing return navigateToSidebarOption(0); @@ -528,7 +529,7 @@ describe('Unread Indicators', () => { return navigateToSidebarOption(0); }) .then(async () => { - await act(() => transitionEndCB && transitionEndCB()); + await act(() => transitionEndCB?.()); const newMessageLineIndicatorHintText = Localize.translateLocal('accessibilityHints.newMessageLineIndicator'); const unreadIndicator = screen.queryAllByLabelText(newMessageLineIndicatorHintText); expect(unreadIndicator).toHaveLength(1); @@ -585,8 +586,8 @@ describe('Unread Indicators', () => { })); it('Displays the correct chat message preview in the LHN when a comment is added then deleted', () => { - let reportActions; - let lastReportAction; + let reportActions: OnyxEntry; + let lastReportAction: ReportAction | undefined; Onyx.connect({ key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, callback: (val) => (reportActions = val), @@ -602,11 +603,11 @@ describe('Unread Indicators', () => { }) .then(() => { // Simulate the response from the server so that the comment can be deleted in this test - lastReportAction = {...CollectionUtils.lastItem(reportActions)}; + lastReportAction = reportActions ? CollectionUtils.lastItem(reportActions) : undefined; Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${REPORT_ID}`, { - lastMessageText: lastReportAction.message[0].text, - lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction.timestamp), - lastActorAccountID: lastReportAction.actorAccountID, + lastMessageText: lastReportAction?.message?.[0].text, + lastVisibleActionCreated: DateUtils.getDBTime(lastReportAction?.timestamp), + lastActorAccountID: lastReportAction?.actorAccountID, reportID: REPORT_ID, }); return waitForBatchedUpdates(); @@ -618,7 +619,9 @@ describe('Unread Indicators', () => { expect(alternateText).toHaveLength(1); expect(alternateText[0].props.children).toBe('Current User Comment 1'); - Report.deleteReportComment(REPORT_ID, lastReportAction); + if (lastReportAction) { + Report.deleteReportComment(REPORT_ID, lastReportAction); + } return waitForBatchedUpdates(); }) .then(() => { diff --git a/tests/utils/getIsUsingFakeTimers.js b/tests/utils/getIsUsingFakeTimers.js deleted file mode 100644 index 376312ac6c06..000000000000 --- a/tests/utils/getIsUsingFakeTimers.js +++ /dev/null @@ -1 +0,0 @@ -export default () => Boolean(global.setTimeout.mock || global.setTimeout.clock); diff --git a/tests/utils/getIsUsingFakeTimers.ts b/tests/utils/getIsUsingFakeTimers.ts new file mode 100644 index 000000000000..52138276928c --- /dev/null +++ b/tests/utils/getIsUsingFakeTimers.ts @@ -0,0 +1,3 @@ +type SetTimeout = typeof global.setTimeout & jest.Mock & typeof jasmine; + +export default () => Boolean((global.setTimeout as SetTimeout).mock || (global.setTimeout as SetTimeout).clock);