From 0cb71215184cfc5f35e5d3358533298a637c95be Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 8 Oct 2026 16:14:27 +0200 Subject: [PATCH 1/3] Replace GUIDE_ACCOUNT_IDS derived value with a personal details selector --- src/ONYXKEYS.ts | 2 - src/hooks/useFindLastAccessedReport.ts | 3 +- src/hooks/useGuideAccountIDs.ts | 10 ++++ src/hooks/useSidebarOrderedReports.tsx | 3 +- src/libs/ReportUtils.ts | 5 +- src/libs/SidebarUtils.ts | 5 +- .../OnyxDerived/ONYX_DERIVED_VALUES.ts | 2 - .../OnyxDerived/configs/guideAccountIDs.ts | 30 ---------- src/pages/Debug/Report/DebugReportPage.tsx | 3 +- .../ReportDetailsMenuLeaveItem.tsx | 3 +- .../ReportDetailsTaskDeleteAction.tsx | 3 +- .../inbox/report/AncestorReportActionItem.tsx | 3 +- .../withReportAndReportActionOrNotFound.tsx | 3 +- .../inbox/report/withReportOrNotFound.tsx | 3 +- src/selectors/PersonalDetails.ts | 18 ++++++ src/types/onyx/DerivedValues.ts | 7 --- src/types/onyx/index.ts | 2 - tests/unit/OnyxDerived/guideAccountIDsTest.ts | 50 ---------------- tests/unit/PersonalDetailsSelectorTest.ts | 59 +++++++++++++++++++ tests/unit/useSidebarOrderedReportsTest.tsx | 21 +++---- 20 files changed, 118 insertions(+), 117 deletions(-) create mode 100644 src/hooks/useGuideAccountIDs.ts delete mode 100644 src/libs/actions/OnyxDerived/configs/guideAccountIDs.ts delete mode 100644 tests/unit/OnyxDerived/guideAccountIDsTest.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index d2b16572d106..74139bdec1bf 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -1411,7 +1411,6 @@ const ONYXKEYS = { CARD_FEED_ERRORS: 'cardFeedErrors', RAM_ONLY_SORTED_REPORT_ACTIONS: 'sortedReportActions', LOGIN_TO_ACCOUNT_ID_MAP: 'loginToAccountIDMap', - GUIDE_ACCOUNT_IDS: 'guideAccountIDs', SPEND_DATA_SIGNATURE: 'spendDataSignature', ONE_ON_ONE_CHAT_REPORT_IDS: 'oneOnOneChatReportIDs', }, @@ -1983,7 +1982,6 @@ type OnyxDerivedValuesMapping = { [ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: OnyxTypes.SortedReportActionsDerivedValue; [ONYXKEYS.DERIVED.LOGIN_TO_ACCOUNT_ID_MAP]: OnyxTypes.LoginToAccountIDMapDerivedValue; [ONYXKEYS.DERIVED.SPEND_DATA_SIGNATURE]: OnyxTypes.SpendDataSignatureDerivedValue; - [ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS]: OnyxTypes.GuideAccountIDsDerivedValue; [ONYXKEYS.DERIVED.ONE_ON_ONE_CHAT_REPORT_IDS]: OnyxTypes.OneOnOneChatReportIDsDerivedValue; }; diff --git a/src/hooks/useFindLastAccessedReport.ts b/src/hooks/useFindLastAccessedReport.ts index c371cb4d6ac0..593bf399e66c 100644 --- a/src/hooks/useFindLastAccessedReport.ts +++ b/src/hooks/useFindLastAccessedReport.ts @@ -4,6 +4,7 @@ import {findLastAccessedReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import useGuideAccountIDs from './useGuideAccountIDs'; import useOnyx from './useOnyx'; import usePermissions from './usePermissions'; @@ -19,7 +20,7 @@ function useFindLastAccessedReport({openOnAdminRoom, enabled = true}: UseFindLas const {isBetaEnabled} = usePermissions(); const ignoreDomainRooms = !isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS); const shouldPreferAdminRoom = openOnAdminRoom ?? shouldOpenOnAdminRoom(); - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const [reportNameValuePairs, reportNameValuePairsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS); const [lastAccessedReportID, reportsMetadata] = useOnyx(ONYXKEYS.COLLECTION.REPORT, { selector: (reports) => (enabled ? findLastAccessedReport(ignoreDomainRooms, guideAccountIDs, shouldPreferAdminRoom, undefined, reportNameValuePairs, reports)?.reportID : undefined), diff --git a/src/hooks/useGuideAccountIDs.ts b/src/hooks/useGuideAccountIDs.ts new file mode 100644 index 000000000000..741c91172c3f --- /dev/null +++ b/src/hooks/useGuideAccountIDs.ts @@ -0,0 +1,10 @@ +import {guideAccountIDsSelector} from '@selectors/PersonalDetails'; + +import {useAllPersonalDetailsWithoutSnapshots} from './usePersonalDetails'; + +function useGuideAccountIDs() { + const [guideAccountIDs] = useAllPersonalDetailsWithoutSnapshots(guideAccountIDsSelector); + return guideAccountIDs; +} + +export default useGuideAccountIDs; diff --git a/src/hooks/useSidebarOrderedReports.tsx b/src/hooks/useSidebarOrderedReports.tsx index 80dd9483a600..8cd6e239e29a 100644 --- a/src/hooks/useSidebarOrderedReports.tsx +++ b/src/hooks/useSidebarOrderedReports.tsx @@ -16,6 +16,7 @@ import React, {createContext, useCallback, useContext, useEffect, useMemo, useRe import useCollectionDelta from './useCollectionDelta'; import {useCurrentReportIDState} from './useCurrentReportID'; import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails'; +import useGuideAccountIDs from './useGuideAccountIDs'; import useLocalize from './useLocalize'; import useNetwork from './useNetwork'; import useOnyx from './useOnyx'; @@ -113,7 +114,7 @@ function SidebarOrderedReportsContextProvider({ const isDefaultRoomsBetaEnabled = isBetaEnabled(CONST.BETAS.DEFAULT_ROOMS); // useOnyx only gives us a new reference when the guide set actually differs, so comparing references // below is enough to detect late guide hydration. - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const prevGuideAccountIDs = usePrevious(guideAccountIDs); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const reportAttributes = useReportAttributes(); diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 439fa74a2dea..2d1e4fca67d1 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -27,7 +27,6 @@ import type { BankAccountList, Card, CardList, - GuideAccountIDsDerivedValue, IntroSelected, OnyxInputOrEntry, OutstandingReportsByPolicyIDDerivedValue, @@ -2500,7 +2499,7 @@ function isHiddenForCurrentUser(reportOrPreference: OnyxEntry | string | * by cross-referencing the accountIDs with personalDetails since guides that are participants * of the user's chats should have their personal details in Onyx. */ -function hasExpensifyGuidesEmails(accountIDs: number[], guideAccountIDs: GuideAccountIDsDerivedValue | undefined): boolean { +function hasExpensifyGuidesEmails(accountIDs: number[], guideAccountIDs: number[] | undefined): boolean { if (guideAccountIDs) { return accountIDs.some((accountID) => guideAccountIDs.includes(accountID)); } @@ -2537,7 +2536,7 @@ function toLastAccessedReport(report: OnyxEntry): LastAccessedReport | u */ function findLastAccessedReport( ignoreDomainRooms: boolean, - guideAccountIDs: GuideAccountIDsDerivedValue | undefined, + guideAccountIDs: number[] | undefined, openOnAdminRoom = false, excludeReportID?: string, reportNameValuePairs?: OnyxCollection, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 956a15f5adcf..987858bac137 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -7,7 +7,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type { Card, - GuideAccountIDsDerivedValue, PersonalDetails, PersonalDetailsList, PolicyTagLists, @@ -279,7 +278,7 @@ function getReportsToDisplayInLHN({ currentUserAccountID: number; reportNameValuePairs?: OnyxCollection; reportAttributes?: ReportAttributesDerivedValue['reports']; - guideAccountIDs?: GuideAccountIDsDerivedValue; + guideAccountIDs?: number[]; conciergeReportID: string | undefined; }) { const isInFocusMode = priorityMode === CONST.PRIORITY_MODE.GSD; @@ -338,7 +337,7 @@ type UpdateReportsToDisplayInLHNProps = { isOffline: boolean; currentUserLogin: string; currentUserAccountID: number; - guideAccountIDs?: GuideAccountIDsDerivedValue; + guideAccountIDs?: number[]; conciergeReportID: string | undefined; }; diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index 4ea4fabd9560..943ae21744ee 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -5,7 +5,6 @@ import type {ValueOf} from 'type-fest'; import type {OnyxDerivedValueConfig} from './types'; import cardFeedErrorsConfig from './configs/cardFeedErrors'; -import guideAccountIDsConfig from './configs/guideAccountIDs'; import loginToAccountIDMapConfig from './configs/loginToAccountIDMap'; import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorkspaceCardList'; import oneOnOneChatReportIDsConfig from './configs/oneOnOneChatReportIDs'; @@ -31,7 +30,6 @@ const ONYX_DERIVED_VALUES = { [ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: cardFeedErrorsConfig, [ONYXKEYS.DERIVED.RAM_ONLY_SORTED_REPORT_ACTIONS]: sortedReportActionsConfig, [ONYXKEYS.DERIVED.LOGIN_TO_ACCOUNT_ID_MAP]: loginToAccountIDMapConfig, - [ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS]: guideAccountIDsConfig, [ONYXKEYS.DERIVED.SPEND_DATA_SIGNATURE]: spendDataSignatureConfig, [ONYXKEYS.DERIVED.ONE_ON_ONE_CHAT_REPORT_IDS]: oneOnOneChatReportIDsConfig, } as const satisfies { diff --git a/src/libs/actions/OnyxDerived/configs/guideAccountIDs.ts b/src/libs/actions/OnyxDerived/configs/guideAccountIDs.ts deleted file mode 100644 index 45b760a68be4..000000000000 --- a/src/libs/actions/OnyxDerived/configs/guideAccountIDs.ts +++ /dev/null @@ -1,30 +0,0 @@ -import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; - -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {GuideAccountIDsDerivedValue} from '@src/types/onyx'; - -import {Str} from 'expensify-common'; - -/** - * Derives the accountIDs of every Expensify Guide (identified by login domain) in the personal details list. - * Sorted so an unrelated personal-details change recomputes to a shallow-equal array. See issue #66413. - */ -export default createOnyxDerivedValueConfig({ - key: ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS, - dependencies: [ONYXKEYS.PERSONAL_DETAILS_LIST], - compute: ([personalDetailsList]) => { - if (!personalDetailsList) { - return []; - } - - const guideAccountIDs: GuideAccountIDsDerivedValue = []; - for (const personalDetails of Object.values(personalDetailsList)) { - if (!personalDetails?.login || Str.extractEmailDomain(personalDetails.login) !== CONST.EMAIL.GUIDES_DOMAIN) { - continue; - } - guideAccountIDs.push(personalDetails.accountID); - } - return guideAccountIDs.sort((a, b) => a - b); - }, -}); diff --git a/src/pages/Debug/Report/DebugReportPage.tsx b/src/pages/Debug/Report/DebugReportPage.tsx index 67b5ea28066d..3e27d9acba53 100644 --- a/src/pages/Debug/Report/DebugReportPage.tsx +++ b/src/pages/Debug/Report/DebugReportPage.tsx @@ -4,6 +4,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useGuideAccountIDs from '@hooks/useGuideAccountIDs'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; @@ -91,7 +92,7 @@ function DebugReportPage({ const [reportOwnerPersonalDetail] = usePersonalDetail(report?.ownerAccountID); const transactionID = DebugUtils.getTransactionID(report, reportActions); const isReportArchived = useReportIsArchived(reportID); - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const hasGuidesEmails = hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number), guideAccountIDs); const derivedIsEmptyReport = useDerivedIsEmptyReport(reportID); diff --git a/src/pages/DynamicReportDetailsPage/ReportDetailsMenuLeaveItem.tsx b/src/pages/DynamicReportDetailsPage/ReportDetailsMenuLeaveItem.tsx index 754fd05565e4..ffaa44efefbe 100644 --- a/src/pages/DynamicReportDetailsPage/ReportDetailsMenuLeaveItem.tsx +++ b/src/pages/DynamicReportDetailsPage/ReportDetailsMenuLeaveItem.tsx @@ -3,6 +3,7 @@ import {ModalActions} from '@components/Modal/Global/ModalContext'; import useConfirmModal from '@hooks/useConfirmModal'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useGuideAccountIDs from '@hooks/useGuideAccountIDs'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; import useOnyx from '@hooks/useOnyx'; @@ -37,7 +38,7 @@ function ReportDetailsMenuLeaveItem({reportID}: ReportDetailsMenuLeaveItemProps) const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`); const [quickAction] = useOnyx(ONYXKEYS.NVP_QUICK_ACTION_GLOBAL_CREATE); const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`); - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED); const [isSelfTourViewed] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {selector: hasSeenTourSelector}); diff --git a/src/pages/DynamicReportDetailsPage/ReportDetailsTaskDeleteAction.tsx b/src/pages/DynamicReportDetailsPage/ReportDetailsTaskDeleteAction.tsx index b8c2339b3f07..d7aae02e617b 100644 --- a/src/pages/DynamicReportDetailsPage/ReportDetailsTaskDeleteAction.tsx +++ b/src/pages/DynamicReportDetailsPage/ReportDetailsTaskDeleteAction.tsx @@ -2,6 +2,7 @@ import MenuItemAction from '@components/MenuItem/presets/MenuItemAction'; import useAncestors from '@hooks/useAncestors'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useGuideAccountIDs from '@hooks/useGuideAccountIDs'; import useHasOutstandingChildTask from '@hooks/useHasOutstandingChildTask'; import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; @@ -46,7 +47,7 @@ function ReportDetailsTaskDeleteActionContent({report, parentReport, parentRepor const expensifyIcons = useMemoizedLazyExpensifyIcons(['Trashcan']); const taskDeleteBackTo = Navigation.getTopmostSearchReportRouteParams()?.backTo; const hasOutstandingChildTask = useHasOutstandingChildTask(report); - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID); const [reportActionsForOriginalReportID] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`); const [delegateEmail] = useOnyx(ONYXKEYS.ACCOUNT, {selector: delegateEmailSelector}); diff --git a/src/pages/inbox/report/AncestorReportActionItem.tsx b/src/pages/inbox/report/AncestorReportActionItem.tsx index caf4dbb51e8d..9e3f44f3775f 100644 --- a/src/pages/inbox/report/AncestorReportActionItem.tsx +++ b/src/pages/inbox/report/AncestorReportActionItem.tsx @@ -1,6 +1,7 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useGuideAccountIDs from '@hooks/useGuideAccountIDs'; import useNetwork from '@hooks/useNetwork'; import useOnyx from '@hooks/useOnyx'; import usePermissions from '@hooks/usePermissions'; @@ -87,7 +88,7 @@ function AncestorReportActionItem({ const styles = useThemeStyles(); const currentUserPersonalDetail = useCurrentUserPersonalDetails(); const [reportOwnerPersonalDetail] = usePersonalDetail(report?.ownerAccountID); - const [guideAccountIDs] = useOnyx(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS); + const guideAccountIDs = useGuideAccountIDs(); const hasGuidesEmails = hasExpensifyGuidesEmails(Object.keys(report?.participants ?? {}).map(Number), guideAccountIDs); const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`, {selector: getStableReportSelector}); diff --git a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx index 0d429096cce4..6f13ea0c2bd8 100644 --- a/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx +++ b/src/pages/inbox/report/withReportAndReportActionOrNotFound.tsx @@ -1,6 +1,7 @@ import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; +import useGuideAccountIDs from '@hooks/useGuideAccountIDs'; import useOnyx from '@hooks/useOnyx'; import useParentReportAction from '@hooks/useParentReportAction'; import usePermissions from '@hooks/usePermissions'; @@ -60,7 +61,7 @@ function WithReportOrNotFoundImpl (personalDetailsList: OnyxEntry) => getPersonalDetailsByID(accountID, personalDetailsList); const multiPersonalDetailsSelector = (accountIDs: number[] | undefined) => (personalDetails: OnyxEntry) => getPersonalDetailsByIDs(accountIDs, personalDetails); @@ -95,6 +97,21 @@ const optimisticPersonalDetailsSelector = (personalDetailsList: OnyxEntry): number[] => { + const guideAccountIDs: number[] = []; + for (const personalDetails of Object.values(personalDetailsList ?? {})) { + if (!personalDetails?.login || Str.extractEmailDomain(personalDetails.login) !== CONST.EMAIL.GUIDES_DOMAIN) { + continue; + } + guideAccountIDs.push(personalDetails.accountID); + } + return guideAccountIDs.sort((a, b) => a - b); +}; + const newAccountIDsAndLoginsSelector = (invitedEmailsToAccountIDs: InvitedEmailsToAccountIDs | undefined) => (personalDetailsList: OnyxEntry) => getNewAccountIDsAndLogins(invitedEmailsToAccountIDs, personalDetailsList); @@ -125,6 +142,7 @@ export { accountIDToLoginSelector, isPersonalDetailOptimistic, optimisticPersonalDetailsSelector, + guideAccountIDsSelector, createDisplayDetailsByAccountIDsSelector, newAccountIDsAndLoginsSelector, displayNameSelector, diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 3f5e2a136157..dfb7a2f0af26 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -219,12 +219,6 @@ type PersonalAndWorkspaceCardListDerivedValue = CardList; */ type LoginToAccountIDMapDerivedValue = Record; -/** - * The accountIDs of every Expensify Guide known to the personal details list, sorted ascending. - * Lets callers check for a guide participant without re-scanning the whole list (see issue #66413). - */ -type GuideAccountIDsDerivedValue = number[]; - /** * Counters that move when spend data changes, used by the Home cards to know a refetch is owed. */ @@ -256,7 +250,6 @@ export type { PersonalAndWorkspaceCardListDerivedValue, CardFeedErrorsDerivedValue, LoginToAccountIDMapDerivedValue, - GuideAccountIDsDerivedValue, SpendDataSignatureDerivedValue, OneOnOneChatReportIDsDerivedValue, CardFeedErrorsObject, diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index fad7cf8b5ab5..a209c6a50ca5 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -60,7 +60,6 @@ import type CustomStatusDraft from './CustomStatusDraft'; import type DatabaseSizeMeasurement from './DatabaseSizeMeasurement'; import type { CardFeedErrorsDerivedValue, - GuideAccountIDsDerivedValue, SpendDataSignatureDerivedValue, LoginToAccountIDMapDerivedValue, NonPersonalAndWorkspaceCardListDerivedValue, @@ -462,7 +461,6 @@ export type { PersonalAndWorkspaceCardListDerivedValue, CardFeedErrorsDerivedValue, LoginToAccountIDMapDerivedValue, - GuideAccountIDsDerivedValue, SpendDataSignatureDerivedValue, OneOnOneChatReportIDsDerivedValue, ScheduleCallDraft, diff --git a/tests/unit/OnyxDerived/guideAccountIDsTest.ts b/tests/unit/OnyxDerived/guideAccountIDsTest.ts deleted file mode 100644 index fd8e7d6ff91f..000000000000 --- a/tests/unit/OnyxDerived/guideAccountIDsTest.ts +++ /dev/null @@ -1,50 +0,0 @@ -import guideAccountIDsConfig from '@libs/actions/OnyxDerived/configs/guideAccountIDs'; - -import CONST from '@src/CONST'; -import type {PersonalDetailsList} from '@src/types/onyx'; - -describe('guideAccountIDs', () => { - const guideAccountID = 8; - const otherGuideAccountID = 3; - const memberAccountID = 1; - const guideLogin = `guide@${CONST.EMAIL.GUIDES_DOMAIN}`; - const otherGuideLogin = `another.guide@${CONST.EMAIL.GUIDES_DOMAIN}`; - - it('returns an empty list when there are no personal details', () => { - expect(guideAccountIDsConfig.compute([undefined], {})).toEqual([]); - expect(guideAccountIDsConfig.compute([{}], {})).toEqual([]); - }); - - it('collects only the accounts whose login is on the guides domain', () => { - const personalDetailsList: PersonalDetailsList = { - [memberAccountID]: {accountID: memberAccountID, login: 'member@example.com'}, - [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, - }; - - expect(guideAccountIDsConfig.compute([personalDetailsList], {})).toEqual([guideAccountID]); - }); - - it('ignores entries without a login', () => { - const personalDetailsList: PersonalDetailsList = { - [memberAccountID]: {accountID: memberAccountID}, - [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, - }; - - expect(guideAccountIDsConfig.compute([personalDetailsList], {})).toEqual([guideAccountID]); - }); - - it('sorts the result so an unrelated personal-details change recomputes to a shallow-equal array', () => { - const guideListedLast: PersonalDetailsList = { - [otherGuideAccountID]: {accountID: otherGuideAccountID, login: otherGuideLogin}, - [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, - }; - const sameGuidesPlusAnAvatarChange: PersonalDetailsList = { - [guideAccountID]: {accountID: guideAccountID, login: guideLogin, avatar: 'https://example.com/avatar.png'}, - [memberAccountID]: {accountID: memberAccountID, login: 'member@example.com'}, - [otherGuideAccountID]: {accountID: otherGuideAccountID, login: otherGuideLogin}, - }; - - expect(guideAccountIDsConfig.compute([guideListedLast], {})).toEqual([otherGuideAccountID, guideAccountID]); - expect(guideAccountIDsConfig.compute([sameGuidesPlusAnAvatarChange], {})).toEqual([otherGuideAccountID, guideAccountID]); - }); -}); diff --git a/tests/unit/PersonalDetailsSelectorTest.ts b/tests/unit/PersonalDetailsSelectorTest.ts index 64f131fc1170..25fe4d6b7e90 100644 --- a/tests/unit/PersonalDetailsSelectorTest.ts +++ b/tests/unit/PersonalDetailsSelectorTest.ts @@ -7,6 +7,7 @@ import type {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; import { createDisplayDetailsByAccountIDsSelector, + guideAccountIDsSelector, multiPersonalDetailsSelector, personalDetailsDisplayNameSelector, personalDetailsListSelector, @@ -240,4 +241,62 @@ describe('PersonalDetailsSelector', () => { expect(result).toEqual({}); }); }); + + describe('guideAccountIDsSelector', () => { + const guideAccountID = 8; + const otherGuideAccountID = 3; + const memberAccountID = 1; + const guideLogin = `guide@${CONST.EMAIL.GUIDES_DOMAIN}`; + const otherGuideLogin = `another.guide@${CONST.EMAIL.GUIDES_DOMAIN}`; + + it('should return an empty list when there are no personal details', () => { + // Given no personal details, or an empty list + // When the guide accountIDs are selected + // Then nothing is a guide + expect(guideAccountIDsSelector(undefined)).toEqual([]); + expect(guideAccountIDsSelector({})).toEqual([]); + }); + + it('should collect only the accounts whose login is on the guides domain', () => { + // Given a member and a guide + const list: PersonalDetailsList = { + [memberAccountID]: {accountID: memberAccountID, login: 'member@example.com'}, + [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, + }; + + // When the guide accountIDs are selected + // Then only the guide is returned, since guides are identified by their login domain + expect(guideAccountIDsSelector(list)).toEqual([guideAccountID]); + }); + + it('should ignore entries without a login', () => { + // Given an entry with no login next to a guide + const list: PersonalDetailsList = { + [memberAccountID]: {accountID: memberAccountID}, + [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, + }; + + // When the guide accountIDs are selected + // Then the entry without a login is skipped, since its domain is unknown + expect(guideAccountIDsSelector(list)).toEqual([guideAccountID]); + }); + + it('should select a deep-equal sorted array when an unrelated personal detail changes', () => { + // Given the same guides in a different key order, plus an avatar change and a new member + const guideListedLast: PersonalDetailsList = { + [otherGuideAccountID]: {accountID: otherGuideAccountID, login: otherGuideLogin}, + [guideAccountID]: {accountID: guideAccountID, login: guideLogin}, + }; + const sameGuidesPlusAnAvatarChange: PersonalDetailsList = { + [guideAccountID]: {accountID: guideAccountID, login: guideLogin, avatar: 'https://example.com/avatar.png'}, + [memberAccountID]: {accountID: memberAccountID, login: 'member@example.com'}, + [otherGuideAccountID]: {accountID: otherGuideAccountID, login: otherGuideLogin}, + }; + + // When the guide accountIDs are selected from both + // Then both are sorted the same, so useOnyx's deep-equal check skips the re-render + expect(guideAccountIDsSelector(guideListedLast)).toEqual([otherGuideAccountID, guideAccountID]); + expect(guideAccountIDsSelector(sameGuidesPlusAnAvatarChange)).toEqual([otherGuideAccountID, guideAccountID]); + }); + }); }); diff --git a/tests/unit/useSidebarOrderedReportsTest.tsx b/tests/unit/useSidebarOrderedReportsTest.tsx index 25a233e49436..d858b4b27ce0 100644 --- a/tests/unit/useSidebarOrderedReportsTest.tsx +++ b/tests/unit/useSidebarOrderedReportsTest.tsx @@ -73,7 +73,7 @@ describe('useSidebarOrderedReports', () => { [ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS]: {}, [ONYXKEYS.BETAS]: [], [ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: {reports: {}}, - [ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS]: [], + [ONYXKEYS.PERSONAL_DETAILS_LIST]: {}, } satisfies OnyxMultiSetInput); }); @@ -356,7 +356,7 @@ describe('useSidebarOrderedReports', () => { await act(async () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}1`, displayedReports['1']); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}2`, domainRoomReport); - await Onyx.set(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS, []); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {}); }); renderHook(() => useSidebarOrderedReports(), { @@ -367,10 +367,9 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.updateReportsToDisplayInLHN.mockClear(); - // The guide's personal details arriving is what turns the derived value from empty into a populated list. - // Deriving that list from the personal details is covered by tests/unit/OnyxDerived/guideAccountIDsTest.ts. + // The guide's personal details arriving is what turns the selected guide accountIDs from empty into a populated list. await act(async () => { - await Onyx.set(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS, [Number(guideAccountID)]); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[guideAccountID]: {accountID: Number(guideAccountID), login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`}}); }); await waitForBatchedUpdatesWithAct(); @@ -382,7 +381,7 @@ describe('useSidebarOrderedReports', () => { ); }); - it('should not recompute all reports when the guide accountIDs are recomputed to the same set', async () => { + it('should not recompute all reports when an unrelated personal-details change keeps the same guide accountIDs', async () => { const participantAccountID = '8'; const displayedReports = createMockReports({ report1: {reportName: 'Chat A'}, @@ -400,7 +399,9 @@ describe('useSidebarOrderedReports', () => { }, }, }); - await Onyx.set(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS, [Number(participantAccountID)]); + // A second report that is not displayed, so a full re-scan would name more than one report key + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}2`, {reportID: '2', reportName: 'Chat B', type: CONST.REPORT.TYPE.CHAT} as Report); + await Onyx.set(ONYXKEYS.PERSONAL_DETAILS_LIST, {[participantAccountID]: {accountID: Number(participantAccountID), login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`}}); }); renderHook(() => useSidebarOrderedReports(), { @@ -411,10 +412,10 @@ describe('useSidebarOrderedReports', () => { mockSidebarUtils.updateReportsToDisplayInLHN.mockClear(); - // An unrelated personal-details change (a new avatar, a display name edit) recomputes the derived value to a - // fresh but shallow-equal array. That must not look like guide hydration and force a full LHN re-scan. + // An unrelated personal-details change (a new avatar) selects a fresh but deep-equal array. + // That must not look like guide hydration and force a full LHN re-scan. await act(async () => { - await Onyx.set(ONYXKEYS.DERIVED.GUIDE_ACCOUNT_IDS, [Number(participantAccountID)]); + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[participantAccountID]: {avatar: 'https://example.com/avatar.png'}}); }); await waitForBatchedUpdatesWithAct(); From 638af2e876c2a38c6f5b3d1de893afc059bc362e Mon Sep 17 00:00:00 2001 From: Hubert Sosinski Date: Thu, 8 Oct 2026 16:30:11 +0200 Subject: [PATCH 2/3] Add reassure test for guide accountIDs on personal details writes --- ...ountIDsPersonalDetailsWrites.perf-test.tsx | 269 ++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 tests/perf-test/GuideAccountIDsPersonalDetailsWrites.perf-test.tsx diff --git a/tests/perf-test/GuideAccountIDsPersonalDetailsWrites.perf-test.tsx b/tests/perf-test/GuideAccountIDsPersonalDetailsWrites.perf-test.tsx new file mode 100644 index 000000000000..65a735f6ef77 --- /dev/null +++ b/tests/perf-test/GuideAccountIDsPersonalDetailsWrites.perf-test.tsx @@ -0,0 +1,269 @@ +import {render, screen} from '@testing-library/react-native'; + +import {setHasRadio} from '@libs/NetworkState'; + +import withReportOrNotFound from '@pages/inbox/report/withReportOrNotFound'; +import type {WithReportOrNotFoundProps} from '@pages/inbox/report/withReportOrNotFound'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList} from '@src/types/onyx'; + +import type * as Navigation from '@react-navigation/native'; + +import React from 'react'; +import {View} from 'react-native'; +import Onyx from 'react-native-onyx'; +import {measureAsyncFunction, measureRenders} from 'reassure'; + +import createMock from '../utils/createMock'; +import * as LHNTestUtils from '../utils/LHNTestUtils'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; + +jest.mock('@libs/Permissions'); +jest.mock('../../src/libs/Navigation/Navigation', () => ({ + navigate: jest.fn(), + isActiveRoute: jest.fn(), + getTopmostReportId: jest.fn(), + getActiveRoute: jest.fn(), + getTopmostReportActionId: jest.fn(), + isNavigationReady: jest.fn(() => Promise.resolve()), + isDisplayedInModal: jest.fn(() => false), + getActiveRouteWithoutParams: jest.fn(() => ''), +})); +jest.mock('../../src/libs/Navigation/navigationRef', () => ({ + getState: () => ({ + routes: [{name: 'Report'}], + }), + getRootState: () => ({ + routes: [], + }), + addListener: () => () => {}, + isReady: () => true, +})); + +// Declared here rather than relying on LHNTestUtils, because withReportOrNotFound loads @react-navigation/native before LHNTestUtils does +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useNavigationState: () => true, + useRoute: jest.fn(), + useFocusEffect: jest.fn(), + useIsFocused: () => true, + useNavigation: () => ({ + navigate: jest.fn(), + addListener: jest.fn(), + }), + createNavigationContainerRef: jest.fn(), + }; +}); + +const REPORT_COUNT = 500; +const REPORT_SCREEN_COUNT = 4; +const LARGE_PERSONAL_DETAILS_COUNT = 5000; +const WRITES_PER_SCENARIO = 10; +const GUIDE_ACCOUNT_ID = 50; +const UNRELATED_ACCOUNT_ID = 60; +const FIRST_EXTRA_ACCOUNT_ID = 1000; +const FIRST_JOINING_GUIDE_ACCOUNT_ID = 100000; + +const reports = Object.fromEntries( + Array.from({length: REPORT_COUNT}, (value, index) => { + const reportID = String(index + 1); + const report = {...LHNTestUtils.getFakeReport([1, 2], 1, true), reportID, lastMessageText: 'hey'}; + return [`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report]; + }), +); + +const smallPersonalDetails: PersonalDetailsList = { + ...LHNTestUtils.fakePersonalDetails, + [GUIDE_ACCOUNT_ID]: {accountID: GUIDE_ACCOUNT_ID, login: `guide@${CONST.EMAIL.GUIDES_DOMAIN}`, displayName: 'Guide'}, + [UNRELATED_ACCOUNT_ID]: {accountID: UNRELATED_ACCOUNT_ID, login: 'unrelated@test.com', displayName: 'Unrelated'}, +}; + +const largePersonalDetails: PersonalDetailsList = { + ...smallPersonalDetails, + ...Object.fromEntries( + Array.from({length: LARGE_PERSONAL_DETAILS_COUNT}, (value, index) => { + const accountID = FIRST_EXTRA_ACCOUNT_ID + index; + return [accountID, {accountID, login: `user${accountID}@test.com`, displayName: `User ${accountID}`}]; + }), + ), +}; + +function ReportScreenContent({route}: WithReportOrNotFoundProps) { + return ; +} + +const ReportScreen = withReportOrNotFound()(ReportScreenContent); + +// The HOC overwrites the report props with what it reads from Onyx, so these placeholders are never used +const injectedPropsMock = createMock>({}); + +const navigationMock = createMock({}); + +/** Report screens read the guide accountIDs but not the report attributes, so their re-renders isolate guide churn */ +function ReportScreens() { + return ( + <> + {Array.from({length: REPORT_SCREEN_COUNT}, (value, index) => ( + ({params: {reportID: String(index + 1)}})} + navigation={navigationMock} + /> + ))} + + ); +} + +/** The LHN plus a few report screens, the guide accountIDs consumers that are mounted together in a normal session */ +function GuideConsumers() { + return ( + <> + + + + ); +} + +async function seed(personalDetails: PersonalDetailsList) { + await Onyx.multiSet({ + [ONYXKEYS.PERSONAL_DETAILS_LIST]: personalDetails, + [ONYXKEYS.BETAS]: [CONST.BETAS.DEFAULT_ROOMS], + [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.GSD, + [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + ...reports, + }); + await waitForBatchedUpdates(); +} + +describe('Guide accountIDs consumers on personal details writes', () => { + beforeAll(() => { + Onyx.init({ + keys: ONYXKEYS, + evictableKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + }); + // Required lazily so LHNTestUtils registers its @react-navigation/native mock before the engine's imports load it + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + require('@userActions/OnyxDerived').default(); + }); + + beforeEach(async () => { + global.fetch = TestHelper.getGlobalFetchMock(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + setHasRadio(true); + await TestHelper.signInWithTestUser(1, 'email1@test.com', undefined, undefined, 'One'); + await waitForBatchedUpdates(); + }); + + afterEach(async () => { + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + test('[GuideAccountIDs] should re-render when a non-guide user is renamed', async () => { + // Given the LHN and four report screens, with a guide and a user who is in none of the reports + await seed(smallPersonalDetails); + + // Reassure runs the scenario several times against one seeded Onyx, so every run writes a name that doesn't exist yet + let run = 0; + const scenario = async () => { + await screen.findByTestId('lhn-options-list'); + + // When that user is renamed repeatedly, which leaves the set of guides unchanged + for (let index = 0; index < WRITES_PER_SCENARIO; index++) { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated ${run}-${index}`}}); + await waitForBatchedUpdates(); + } + run++; + }; + + // Then reassure measures what the unrelated writes cost the consumers + await measureRenders(, {scenario}); + }); + + test('[GuideAccountIDs] should not re-render report screens when a non-guide user is renamed', async () => { + // Given only the report screens, which subscribe to the guide accountIDs but not to the report attributes + await seed(smallPersonalDetails); + + let run = 0; + const scenario = async () => { + await screen.findAllByTestId(/report-screen-/); + + // When a non-guide user is renamed repeatedly, which leaves the set of guides unchanged + for (let index = 0; index < WRITES_PER_SCENARIO; index++) { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated isolated ${run}-${index}`}}); + await waitForBatchedUpdates(); + } + run++; + }; + + // Then every extra render here comes from the guide accountIDs changing reference without changing content + await measureRenders(, {scenario}); + }); + + test('[GuideAccountIDs] should re-render when a non-guide user is renamed in a large personal details list', async () => { + // Given the same screens, with 5000 more people in the personal details list + await seed(largePersonalDetails); + + let run = 0; + const scenario = async () => { + await screen.findByTestId('lhn-options-list'); + + // When a non-guide user is renamed repeatedly, so every write re-scans the whole list for guides + for (let index = 0; index < WRITES_PER_SCENARIO; index++) { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated ${run}-${index}`}}); + await waitForBatchedUpdates(); + } + run++; + }; + + // Then reassure measures the render cost, which grows if each consumer re-scans the list during render + await measureRenders(, {scenario}); + }); + + test('[GuideAccountIDs] should re-render when a new guide joins', async () => { + // Given the LHN and four report screens + await seed(smallPersonalDetails); + + let nextGuideAccountID = FIRST_JOINING_GUIDE_ACCOUNT_ID; + const scenario = async () => { + await screen.findByTestId('lhn-options-list'); + + // When a new guide appears on every write, so the set of guides really changes and the LHN must re-scan + for (let index = 0; index < WRITES_PER_SCENARIO; index++) { + const accountID = nextGuideAccountID++; + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[accountID]: {accountID, login: `guide${accountID}@${CONST.EMAIL.GUIDES_DOMAIN}`}}); + await waitForBatchedUpdates(); + } + }; + + // Then reassure measures the cost of a real guide change, which both versions have to pay + await measureRenders(, {scenario}); + }); + + test('[GuideAccountIDs] should process non-guide renames in a large personal details list', async () => { + // Given the screens mounted over the large personal details list + await seed(largePersonalDetails); + render(); + await screen.findByTestId('lhn-options-list'); + + // When a non-guide user is renamed repeatedly + let run = 0; + const writeRenames = async () => { + for (let index = 0; index < WRITES_PER_SCENARIO; index++) { + await Onyx.merge(ONYXKEYS.PERSONAL_DETAILS_LIST, {[UNRELATED_ACCOUNT_ID]: {displayName: `Unrelated timed ${run}-${index}`}}); + await waitForBatchedUpdates(); + } + run++; + }; + + // Then reassure times the whole write loop, including work done outside React renders such as derived computes and their writes + await measureAsyncFunction(writeRenames); + }); +}); From 9ef3c3a699283cfcdfa3c3b4af17467a851c6298 Mon Sep 17 00:00:00 2001 From: truph01 Date: Thu, 15 Oct 2026 02:24:34 +0700 Subject: [PATCH 3/3] Show empty state instead of No results when default view hides all rows --- src/components/Table/TableBody.tsx | 7 +++- .../TableEmptyStates/TableNoResultsState.tsx | 5 ++- tests/ui/TableTest.tsx | 38 +++++++++++++++++++ 3 files changed, 46 insertions(+), 4 deletions(-) diff --git a/src/components/Table/TableBody.tsx b/src/components/Table/TableBody.tsx index 9a4db2f7aead..faa96b4da20f 100644 --- a/src/components/Table/TableBody.tsx +++ b/src/components/Table/TableBody.tsx @@ -164,6 +164,7 @@ function TableBodyList({contentContainerStyle, emptyMessage, onLayout, style, .. noResultsStateElement, tableListMetadata, isEmptyResult, + isDefaultViewEmpty, scrollWidth, tableWidth, } = useTableContext(); @@ -337,8 +338,10 @@ function TableBodyList({contentContainerStyle, emptyMessage, onLayout, style, .. ); - const emptyStateContent = - tableListMetadata.hasPageHeader && isEmptyResult ? (noResultsStateElement ?? EmptyResultComponent) : (emptyStateElement ?? renderListComponent(ListEmptyComponent)); + // When the default view hides every row (e.g. all workspaces are archived), no search or filter is active, + // so show the empty state rather than "No results". + const shouldShowNoResultsState = tableListMetadata.hasPageHeader && isEmptyResult && !isDefaultViewEmpty; + const emptyStateContent = shouldShowNoResultsState ? (noResultsStateElement ?? EmptyResultComponent) : (emptyStateElement ?? renderListComponent(ListEmptyComponent)); const footerElement = renderListComponent(ListFooterComponent); // Consumer footer flex growth is useful below normal rows, but inside the combined empty-state // footer it can expand over the page header. Preserve the remaining style while disabling growth. diff --git a/src/components/Table/TableEmptyStates/TableNoResultsState.tsx b/src/components/Table/TableEmptyStates/TableNoResultsState.tsx index 60a93ca4a2c6..30c49a66fe35 100644 --- a/src/components/Table/TableEmptyStates/TableNoResultsState.tsx +++ b/src/components/Table/TableEmptyStates/TableNoResultsState.tsx @@ -21,10 +21,11 @@ type TableNoResultsStateProps = Omit { expect(screen.queryByTestId('row-2')).toBeNull(); expect(screen.getByTestId('generic-empty-state')).toBeTruthy(); }); + + it('renders the empty state instead of "No results" when a page-header table hides every row by default', () => { + // Given a page-header table (like the Workspaces list) whose default filter hides every row, with both + // a no-results state and an empty state declared + const props = createDefaultProps(); + const data: TestItem[] = [ + {keyForList: '1', id: '1', name: 'Archived A', category: 'archived', value: 1}, + {keyForList: '2', id: '2', name: 'Archived B', category: 'archived', value: 2}, + ]; + + // When the table renders with no search or filter selected + render( + + data={data} + columns={props.columns} + renderItem={props.renderItem} + keyExtractor={props.keyExtractor} + filters={statusFilterConfig} + isItemInFilter={isItemInStatusFilter} + > + + Page header + + + + + + , + ); + + // Then the empty state shows inside the list with the page header still visible, because the user did + // not search or filter, so "No results found" would be misleading + expect(screen.getByTestId('flash-list')).toBeTruthy(); + expect(screen.getByTestId('table-header-component')).toBeTruthy(); + expect(screen.getAllByTestId('generic-empty-state')).toHaveLength(1); + expect(screen.getByText('No workspaces')).toBeTruthy(); + expect(screen.queryByText('common.noResultsFound')).toBeNull(); + }); }); describe('immediate filter', () => {