diff --git a/config/oxlint/oxlint.seatbelt.tsv b/config/oxlint/oxlint.seatbelt.tsv index 029cb4657995..3e6528328636 100644 --- a/config/oxlint/oxlint.seatbelt.tsv +++ b/config/oxlint/oxlint.seatbelt.tsv @@ -1517,7 +1517,6 @@ "../../src/pages/settings/Wallet/AddCardToDigitalWalletPage.tsx" "rulesdir/no-onyx-loading-status" 1 "../../src/pages/settings/Wallet/AddDebitCardPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Wallet/BankAccountPurposePage/index.tsx" "no-restricted-imports" 1 -"../../src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 "../../src/pages/settings/Wallet/BankAccountPurposePage/substeps/CountrySelection.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/settings/Wallet/CardAddedToWalletPage.tsx" "no-restricted-imports" 1 "../../src/pages/settings/Wallet/ChooseTransferAccountPage.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 @@ -1890,7 +1889,6 @@ "../../src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 "../../src/pages/workspace/expensifyCard/DynamicWorkspaceExpensifyCardDetailsPage.tsx" "no-restricted-imports" 1 "../../src/pages/workspace/expensifyCard/DynamicWorkspaceSettlementAccountPage.tsx" "no-restricted-imports" 1 -"../../src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx" "@typescript-eslint/no-deprecated/MenuItem" 2 "../../src/pages/workspace/expensifyCard/issueNew/LimitTypeStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/workspace/expensifyCard/issueNew/spendRules/SetSpendRulesStep.tsx" "react-hooks/purity" 2 @@ -1899,7 +1897,6 @@ "../../src/pages/workspace/expensifyCard/WorkspaceCardSettingsPage.tsx" "no-restricted-imports" 1 "../../src/pages/workspace/expensifyCard/WorkspaceCardsListLabel.tsx" "react-hooks/purity" 1 "../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardAddWorkEmailPage.tsx" "no-restricted-imports" 1 -"../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 "../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1 "../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx" "no-restricted-imports" 1 "../../src/pages/workspace/expensifyCard/WorkspaceExpensifyCardFeedSelectorPage.tsx" "no-restricted-imports" 1 @@ -1964,9 +1961,7 @@ "../../src/pages/workspace/rules/AgentRules/AddAgentRulePage.tsx" "react/no-unstable-nested-components" 1 "../../src/pages/workspace/rules/FlagForReviewRules/FlagForReviewRuleAmountPageBase.tsx" "@typescript-eslint/no-unnecessary-type-assertion" 1 "../../src/pages/workspace/rules/IndividualExpenseRulesSectionRevamp.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 -"../../src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 "../../src/pages/workspace/rules/MerchantRules/MerchantRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 -"../../src/pages/workspace/rules/RulesNewPage.tsx" "@typescript-eslint/no-deprecated/MenuItem" 1 "../../src/pages/workspace/rules/SpendRules/SpendRulePageBase.tsx" "@typescript-eslint/no-deprecated/MenuItemWithTopDescription" 1 "../../src/pages/workspace/tags/DynamicEditTagPage.tsx" "no-restricted-imports" 1 "../../src/pages/workspace/tags/DynamicTagApproverPage.tsx" "no-restricted-imports" 1 diff --git a/src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx b/src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx index a51c356184c4..d875b28dea0d 100644 --- a/src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx +++ b/src/pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose.tsx @@ -1,4 +1,5 @@ import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; +import Icon from '@components/Icon'; import MenuItem from '@components/MenuItem'; import Text from '@components/Text'; @@ -14,10 +15,12 @@ import Navigation from '@navigation/Navigation'; import variables from '@styles/variables'; import {openPersonalBankAccountSetupView} from '@userActions/BankAccounts'; +import {callIfAuthenticated} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {DYNAMIC_ROUTES} from '@src/ROUTES'; +import {authTokenTypeSelector} from '@src/selectors/Session'; import {isCollectingDepositAccountsSelector} from '@selectors/Policy'; import React from 'react'; @@ -33,6 +36,7 @@ function BankAccountPurpose({showCountrySelectionStep}: BankAccountPurposeProps) const {translate} = useLocalize(); const illustrations = useMemoizedLazyIllustrations(['BankCoin', 'WalletAlt2']); const [isCollectingDepositAccounts = false] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {selector: isCollectingDepositAccountsSelector}); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); // A workspace collecting deposit accounts needs the bank details its country requires, which the wallet setup does not ask for. const onGetReimbursedPressed = () => { @@ -47,36 +51,58 @@ function BankAccountPurpose({showCountrySelectionStep}: BankAccountPurposeProps) {translate('bankAccount.bankAccountPurposeTitle')} - - + + + + + + + + {translate('bankAccount.getReimbursed')} + {translate('bankAccount.getReimbursedDescription')} + + + + + + + + onPress={callIfAuthenticated(showCountrySelectionStep, session)} + style={styles.purposeMenuItem} + > + + + + + + + + {translate('bankAccount.makePayments')} + {translate('bankAccount.makePaymentsDescription')} + + + + + + ); diff --git a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx index 36bc6d9cec70..96f821727006 100644 --- a/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx +++ b/src/pages/workspace/expensifyCard/WorkspaceExpensifyCardBankAccounts.tsx @@ -2,6 +2,7 @@ import BankAccountVerificationView from '@components/BankAccountVerificationView import FullPageOfflineBlockingView from '@components/BlockingViews/FullPageOfflineBlockingView'; import DelegateNoAccessWrapper from '@components/DelegateNoAccessWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; +import Icon from '@components/Icon'; import getBankIcon from '@components/Icon/BankIcons'; import MenuItem from '@components/MenuItem'; import MenuItemAction from '@components/MenuItem/presets/MenuItemAction'; @@ -29,11 +30,13 @@ import type {SettingsNavigatorParamList} from '@navigation/types'; import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import {clearIssueNewCardFormData, configureExpensifyCardsForPolicy, setIssueNewCardStepAndData} from '@userActions/Card'; +import {callIfAuthenticated} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {authTokenTypeSelector} from '@src/selectors/Session'; import type {BankName} from '@src/types/onyx/Bank'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -46,6 +49,7 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA const {translate} = useLocalize(); const styles = useThemeStyles(); const [bankAccountsList] = useOnyx(ONYXKEYS.BANK_ACCOUNT_LIST); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); const [supportedCountriesByCurrency] = useOnyx(ONYXKEYS.CARD_SUPPORTED_COUNTRIES); const {login: currentUserLogin = ''} = useCurrentUserPersonalDetails(); @@ -105,19 +109,30 @@ function WorkspaceExpensifyCardBankAccounts({route}: WorkspaceExpensifyCardBankA const {icon, iconSize, iconStyles} = getBankIcon({bankName, styles}); return ( - handleSelectBankAccount(bankAccountID)} - icon={icon} - iconHeight={iconSize} - iconWidth={iconSize} - iconStyles={iconStyles} - shouldShowRightIcon - displayInDefaultIconColor - /> + onPress={callIfAuthenticated(() => handleSelectBankAccount(bankAccountID), session)} + > + + + + + + + + {bankName} + {`${translate('workspace.expensifyCard.accountEndingIn')} ${getLastFourDigits(bankAccountNumber)}`} + + + + + + ); }); }; diff --git a/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx b/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx index 49fd3ef06892..6b2dbac47303 100644 --- a/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx +++ b/src/pages/workspace/expensifyCard/issueNew/CardTypeStep.tsx @@ -1,3 +1,4 @@ +import Icon from '@components/Icon'; import InteractiveStepWrapper from '@components/InteractiveStepWrapper'; import MenuItem from '@components/MenuItem'; import Text from '@components/Text'; @@ -13,8 +14,11 @@ import Navigation from '@libs/Navigation/Navigation'; import variables from '@styles/variables'; +import {callIfAuthenticated} from '@userActions/Session'; + import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {authTokenTypeSelector} from '@src/selectors/Session'; import type {Policy} from '@src/types/onyx'; import type {OnyxEntry} from 'react-native-onyx'; @@ -37,6 +41,7 @@ function CardTypeStep({policy, stepNames, startStepIndex}: CardTypeStepProps) { const illustrations = useMemoizedLazyIllustrations(['HandCard', 'VirtualCard']); const policyID = policy?.id; const [issueNewCard] = useOnyx(`${ONYXKEYS.COLLECTION.RAM_ONLY_ISSUE_NEW_EXPENSIFY_CARD}${policyID}`); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); const isEditing = issueNewCard?.isEditing; @@ -82,32 +87,54 @@ function CardTypeStep({policy, stepNames, startStepIndex}: CardTypeStepProps) { > {translate('workspace.card.issueNewCard.chooseCardType')} - submit(CONST.EXPENSIFY_CARD.CARD_TYPE.PHYSICAL)} - displayInDefaultIconColor - iconStyles={[styles.ml3, styles.mr2]} - iconWidth={variables.menuIconSize} - iconHeight={variables.menuIconSize} - wrapperStyle={styles.purposeMenuItem} - /> - submit(CONST.EXPENSIFY_CARD.CARD_TYPE.PHYSICAL), session)} + style={styles.purposeMenuItem} + > + + + + + + + + {translate('workspace.card.issueNewCard.physicalCard')} + {translate('workspace.card.issueNewCard.physicalCardDescription')} + + + + + + + submit(CONST.EXPENSIFY_CARD.CARD_TYPE.VIRTUAL)} - displayInDefaultIconColor - iconStyles={[styles.ml3, styles.mr2]} - iconWidth={variables.menuIconSize} - iconHeight={variables.menuIconSize} - wrapperStyle={styles.purposeMenuItem} - /> + onPress={callIfAuthenticated(() => submit(CONST.EXPENSIFY_CARD.CARD_TYPE.VIRTUAL), session)} + style={styles.purposeMenuItem} + > + + + + + + + + {translate('workspace.card.issueNewCard.virtualCard')} + {translate('workspace.card.issueNewCard.virtualCardDescription')} + + + + + + ); diff --git a/src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx b/src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx index 5395a2f99d65..45b5afc1598a 100644 --- a/src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx +++ b/src/pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage.tsx @@ -1,5 +1,7 @@ import HeaderWithBackButtonAndTitle from '@components/Header/composed/HeaderWithBackButtonAndTitle'; +import Icon from '@components/Icon'; import MenuItem from '@components/MenuItem'; +import {useMenuItemInteraction} from '@components/MenuItem/MenuItemContext'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; @@ -7,7 +9,9 @@ import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyExpensifyIcons, useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {setDraftMerchantRule} from '@libs/actions/User'; @@ -20,9 +24,13 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; +import {callIfAuthenticated} from '@userActions/Session'; + import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type SCREENS from '@src/SCREENS'; +import {authTokenTypeSelector} from '@src/selectors/Session'; import type {ExpenseDefaultRuleType} from '@src/types/form/MerchantRuleForm'; import React from 'react'; @@ -30,6 +38,21 @@ import {View} from 'react-native'; type ExpenseDefaultTypePageProps = PlatformStackScreenProps; +function ExpenseDefaultLockIcon() { + const theme = useTheme(); + const {isHovered, isPressed} = useMenuItemInteraction(); + const icons = useMemoizedLazyExpensifyIcons(['Lock']); + + return ( + + ); +} + /** * Picks what an expense default matches on before the editor opens, so the editor can drop the condition and the * defaults the chosen type can't carry instead of showing them locked. @@ -39,8 +62,8 @@ function ExpenseDefaultTypePage({route}: ExpenseDefaultTypePageProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const policy = usePolicy(policyID); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); const illustrations = useMemoizedLazyIllustrations(['FoodTruck', 'FolderOpen']); - const expensifyIcons = useMemoizedLazyExpensifyIcons(['Lock']); const {showConfirmModal} = useConfirmModal(); @@ -110,20 +133,29 @@ function ExpenseDefaultTypePage({route}: ExpenseDefaultTypePageProps) { {translate('workspace.rules.expenseDefaultType.subtitle')} {options.map((option) => ( - + onPress={callIfAuthenticated(option.onPress, session)} + style={styles.rulesNewMenuItem} + > + + + + + + + + {option.title} + {option.description} + + {option.isLocked ? : } + + ))} diff --git a/src/pages/workspace/rules/RulesNewPage.tsx b/src/pages/workspace/rules/RulesNewPage.tsx index 650c12ec2d4a..496bf4e8a0f2 100644 --- a/src/pages/workspace/rules/RulesNewPage.tsx +++ b/src/pages/workspace/rules/RulesNewPage.tsx @@ -1,4 +1,5 @@ import HeaderWithBackButtonAndTitle from '@components/Header/composed/HeaderWithBackButtonAndTitle'; +import Icon from '@components/Icon'; import MenuItem from '@components/MenuItem'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; @@ -7,6 +8,7 @@ import Text from '@components/Text'; import useConfirmModal from '@hooks/useConfirmModal'; import {useMemoizedLazyIllustrations} from '@hooks/useLazyAsset'; import useLocalize from '@hooks/useLocalize'; +import useOnyx from '@hooks/useOnyx'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -20,9 +22,13 @@ import AccessOrNotFoundWrapper from '@pages/workspace/AccessOrNotFoundWrapper'; import variables from '@styles/variables'; +import {callIfAuthenticated} from '@userActions/Session'; + import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES, {DYNAMIC_ROUTES} from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; +import {authTokenTypeSelector} from '@src/selectors/Session'; import type IconAsset from '@src/types/utils/IconAsset'; import type {SentryLabel} from '@src/types/utils/SentryLabel'; @@ -54,6 +60,7 @@ function RulesNewPage({route}: RulesNewPageProps) { const isCategorySettingsFlow = route.name === SCREENS.WORKSPACE.DYNAMIC_CATEGORY_RULES_NEW; const isCategoryScopedCreate = isCategorySettingsFlow || !!categoryName; const policy = usePolicy(policyID); + const [session] = useOnyx(ONYXKEYS.SESSION, {selector: authTokenTypeSelector}); const {showConfirmModal} = useConfirmModal(); // From category settings the only expense default is the category's tax rate, so the type chooser is skipped. It @@ -126,19 +133,31 @@ function RulesNewPage({route}: RulesNewPageProps) { const shouldShowDescribeSection = !isCategoryScopedCreate; const newRuleMenuItems = visibleNewRuleOptions.map((option) => ( - + > + + + + + + + + {option.title} + {option.description} + + + + + + )); return ( diff --git a/tests/ui/IllustratedMenuRowsTest.tsx b/tests/ui/IllustratedMenuRowsTest.tsx new file mode 100644 index 000000000000..5d50c4f990b4 --- /dev/null +++ b/tests/ui/IllustratedMenuRowsTest.tsx @@ -0,0 +1,183 @@ +/** Guards illustration colors and the locked expense-default trailing icon during MenuItem migration. */ +import {fireEvent, render, screen} from '@testing-library/react-native'; + +import Icon from '@components/Icon'; +import {LocaleContextProvider} from '@components/LocaleContextProvider'; +import MenuItem from '@components/MenuItem'; +import ThemeProvider from '@components/ThemeProvider'; + +import {hasSelectableCategoryTaxRate} from '@libs/CategoryTaxRulesUtils'; +import Navigation from '@libs/Navigation/Navigation'; +import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; +import type {SettingsNavigatorParamList} from '@libs/Navigation/types'; + +import BankAccountPurpose from '@pages/settings/Wallet/BankAccountPurposePage/substeps/BankAccountPurpose'; +import CardTypeStep from '@pages/workspace/expensifyCard/issueNew/CardTypeStep'; +import ExpenseDefaultTypePage from '@pages/workspace/rules/MerchantRules/ExpenseDefaultTypePage'; + +import variables from '@styles/variables'; + +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import ROUTES from '@src/ROUTES'; +import SCREENS from '@src/SCREENS'; + +import type {PropsWithChildren} from 'react'; + +import React from 'react'; +import Onyx from 'react-native-onyx'; + +import createMock from '../utils/createMock'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; + +const mockShowConfirmModal = jest.fn(); + +jest.mock('@components/Icon', () => jest.fn(() => null)); +jest.mock( + '@components/InteractiveStepWrapper', + () => + ({children}: PropsWithChildren) => + children, +); +jest.mock( + '@components/ScreenWrapper', + () => + ({children}: PropsWithChildren) => + children, +); +jest.mock( + '@components/ScrollView', + () => + ({children}: PropsWithChildren) => + children, +); +jest.mock( + '@components/BlockingViews/FullPageOfflineBlockingView', + () => + ({children}: PropsWithChildren) => + children, +); +jest.mock( + '@pages/workspace/AccessOrNotFoundWrapper', + () => + ({children}: PropsWithChildren) => + children, +); +jest.mock('@components/Header/composed/HeaderWithBackButtonAndTitle', () => () => null); +jest.mock('@hooks/usePolicy', () => () => undefined); +jest.mock('@hooks/useConfirmModal', () => () => ({showConfirmModal: mockShowConfirmModal})); +jest.mock('@libs/Navigation/Navigation', () => ({navigate: jest.fn()})); +jest.mock('@libs/CategoryTaxRulesUtils', () => ({hasUsableTaxRates: () => false, hasSelectableCategoryTaxRate: jest.fn(() => false)})); +jest.mock('@userActions/Session', () => ({callIfAuthenticated: (callback: () => void) => callback})); + +function assertLeadingIllustrations(count: number, size: number) { + const leadingCells = screen.UNSAFE_getAllByType(MenuItem.Leading); + expect(leadingCells).toHaveLength(count); + for (const cell of leadingCells) { + const [illustration] = cell.findAll((node) => node.type === Icon); + expect(illustration).toBeDefined(); + expect(illustration.props.fill).toBeUndefined(); + expect(illustration.props.width).toBe(size); + expect(illustration.props.height).toBe(size); + } +} + +type ExpenseDefaultTypeProps = PlatformStackScreenProps; + +function renderExpenseDefaults() { + const props = createMock({route: {key: 'expense-defaults', name: SCREENS.WORKSPACE.RULES_EXPENSE_DEFAULT_TYPE, params: {policyID: '1'}}}); + return render( + + + + + , + ); +} + +describe('Illustrated composable menu rows', () => { + beforeAll(() => Onyx.init({keys: ONYXKEYS})); + + beforeEach(async () => { + jest.clearAllMocks(); + jest.mocked(hasSelectableCategoryTaxRate).mockReturnValue(false); + await Onyx.clear(); + await waitForBatchedUpdates(); + }); + + it('preserves the two bank-purpose illustrations and the payments callback', () => { + // Given the bank-purpose choices with their existing country-selection callback. + const showCountrySelectionStep = jest.fn(); + render( + + + , + ); + + // When the two choices are composed and Make payments is pressed. + assertLeadingIllustrations(2, variables.menuIconSize); + const roots = screen.UNSAFE_getAllByType(MenuItem.Root); + fireEvent.press(roots[1], {nativeEvent: {}}); + + // Then original colors/sizing, both chevrons, and the selection callback are retained. + expect(screen.UNSAFE_getAllByType(MenuItem.Chevron)).toHaveLength(2); + expect(showCountrySelectionStep).toHaveBeenCalledTimes(1); + }); + + it('preserves physical and virtual card illustrations and text slots', () => { + // Given the card-type step before a card type is selected. + render( + + + , + ); + + // When both card choices are rendered. + assertLeadingIllustrations(2, variables.menuIconSize); + + // Then each retains its title, description, and chevron without forcing an illustration fill. + expect(screen.UNSAFE_getAllByType(MenuItem.Title)).toHaveLength(2); + expect(screen.UNSAFE_getAllByType(MenuItem.Description)).toHaveLength(2); + expect(screen.UNSAFE_getAllByType(MenuItem.Chevron)).toHaveLength(2); + const roots = screen.UNSAFE_getAllByType(MenuItem.Root); + expect(roots[0].props.sentryLabel).toBe(CONST.SENTRY_LABEL.WORKSPACE.EXPENSIFY_CARD.ISSUE_CARD_TYPE_PHYSICAL); + expect(roots[1].props.sentryLabel).toBe(CONST.SENTRY_LABEL.WORKSPACE.EXPENSIFY_CARD.ISSUE_CARD_TYPE_VIRTUAL); + }); + + it('replaces the locked Category chevron with a Lock and keeps its explainer pressable', () => { + // Given a workspace without selectable category tax rates. + renderExpenseDefaults(); + + // When the locked Category choice is pressed. + assertLeadingIllustrations(2, variables.iconSizeExtraLarge); + const category = screen.UNSAFE_getAllByType(MenuItem.Root)[1]; + fireEvent.press(category, {nativeEvent: {}}); + + // Then only Merchant has a chevron, and Category opens its existing explainer rather than navigating. + expect(screen.UNSAFE_getAllByType(MenuItem.Chevron)).toHaveLength(1); + const [trailingLock] = screen.UNSAFE_getAllByType(MenuItem.Trailing)[1].findAll((node) => node.type === Icon); + expect(trailingLock.props.width).toBe(variables.iconSizeNormal); + const [themedChevronIcon] = screen.UNSAFE_getByType(MenuItem.Chevron).findAll((node) => node.type === Icon); + expect(trailingLock.props.fill).toBeDefined(); + expect(trailingLock.props.fill).toBe(themedChevronIcon.props.fill); + expect(mockShowConfirmModal).toHaveBeenCalled(); + expect(Navigation.navigate).not.toHaveBeenCalled(); + }); + + it('shows two chevrons when the Category option is unlocked', () => { + // Given a workspace with a selectable tax rate. + jest.mocked(hasSelectableCategoryTaxRate).mockReturnValue(true); + + // When expense-default choices are rendered and Category is selected. + renderExpenseDefaults(); + fireEvent.press(screen.UNSAFE_getAllByType(MenuItem.Root)[1], {nativeEvent: {}}); + + // Then no locked replacement remains, and the existing editor route opens. + expect(screen.UNSAFE_getAllByType(MenuItem.Chevron)).toHaveLength(2); + expect(Navigation.navigate).toHaveBeenCalledWith(ROUTES.RULES_MERCHANT_NEW.getRoute('1')); + }); +}); diff --git a/tests/ui/RulesNewPageDescribeRuleTest.tsx b/tests/ui/RulesNewPageDescribeRuleTest.tsx index dfff13e22dbd..eb4304d9a325 100644 --- a/tests/ui/RulesNewPageDescribeRuleTest.tsx +++ b/tests/ui/RulesNewPageDescribeRuleTest.tsx @@ -101,15 +101,6 @@ jest.mock('@components/Text', () => { const {Text} = jest.requireActual<{Text: React.ComponentType<{children?: React.ReactNode}>}>('react-native'); return ({children}: {children: React.ReactNode}) => ReactModule.createElement(Text, null, children); }); -jest.mock('@components/MenuItem', () => { - const ReactModule = jest.requireActual('react'); - const {Pressable, Text} = jest.requireActual<{ - Pressable: React.ComponentType<{accessibilityLabel?: string; accessibilityRole?: string; onPress?: () => void; children?: React.ReactNode}>; - Text: React.ComponentType<{children?: React.ReactNode}>; - }>('react-native'); - return ({title, onPress}: {title: string; onPress: () => void}) => - ReactModule.createElement(Pressable, {accessibilityLabel: title, accessibilityRole: 'button', onPress}, ReactModule.createElement(Text, null, title)); -}); jest.mock('@components/Button', () => { const ReactModule = jest.requireActual('react'); const {Pressable, Text} = jest.requireActual<{ @@ -243,7 +234,7 @@ describe('New rule describe section', () => { ]; expect(options).toHaveLength(expectedOptionNames.length); for (const [index, name] of expectedOptionNames.entries()) { - expect(options.at(index)).toHaveAccessibleName(name); + expect(options.at(index)).toHaveAccessibleName(`${name}, ${name}Description`); } // And the describe section follows, with its own submit button