Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Filter deleted workspace categories #54613

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/components/ButtonWithDropdownMenu/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,16 @@ function ButtonWithDropdownMenu<IValueType>({
shouldUseStyleUtilityForAnchorPosition = false,
defaultSelectedIndex = 0,
shouldShowSelectedItemCheck = false,
testID,
}: ButtonWithDropdownMenuProps<IValueType>) {
const theme = useTheme();
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectedItemIndex, setSelectedItemIndex] = useState(defaultSelectedIndex);
const [isMenuVisible, setIsMenuVisible] = useState(false);
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState<AnchorPosition | null>(null);
// In tests, skip the popover anchor position calculation. The default values are needed for popover menu to be rendered in tests.
const defaultPopoverAnchorPosition = process.env.NODE_ENV === 'test' ? {horizontal: 100, vertical: 100} : null;
const [popoverAnchorPosition, setPopoverAnchorPosition] = useState<AnchorPosition | null>(defaultPopoverAnchorPosition);
const {windowWidth, windowHeight} = useWindowDimensions();
const dropdownAnchor = useRef<View | null>(null);
// eslint-disable-next-line react-compiler/react-compiler
Expand Down Expand Up @@ -139,6 +142,7 @@ function ButtonWithDropdownMenu<IValueType>({
iconRight={Expensicons.DownArrow}
shouldShowRightIcon={!isSplitButton}
isSplitButton={isSplitButton}
testID={testID}
/>

{isSplitButton && (
Expand Down
3 changes: 3 additions & 0 deletions src/components/ButtonWithDropdownMenu/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,9 @@ type ButtonWithDropdownMenuProps<TValueType> = {

/** Whether selected items should be marked as selected */
shouldShowSelectedItemCheck?: boolean;

/** Used to locate the component in the tests */
testID?: string;
};

export type {
Expand Down
1 change: 1 addition & 0 deletions src/components/ConfirmContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,7 @@ function ConfirmContent({
isPressOnEnterActive={isVisible}
large
text={confirmText || translate('common.yes')}
accessibilityLabel={confirmText || translate('common.yes')}
isDisabled={isOffline && shouldDisableConfirmButtonWhenOffline}
/>
{shouldShowCancelButton && !shouldReverseStackedButtons && (
Expand Down
5 changes: 5 additions & 0 deletions src/components/MenuItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,9 @@ type MenuItemBaseProps = {

/** Should break word for room title */
shouldBreakWord?: boolean;

/** Pressable component Test ID. Used to locate the component in tests. */
pressableTestID?: string;
};

type MenuItemProps = (IconProps | AvatarProps | NoIcon) & MenuItemBaseProps;
Expand Down Expand Up @@ -461,6 +464,7 @@ function MenuItem(
onHideTooltip,
shouldIconUseAutoWidthStyle = false,
shouldBreakWord = false,
pressableTestID,
}: MenuItemProps,
ref: PressableRef,
) {
Expand Down Expand Up @@ -610,6 +614,7 @@ function MenuItem(
wrapperStyle={outerWrapperStyle}
activeOpacity={variables.pressDimValue}
opacityAnimationDuration={0}
testID={pressableTestID}
style={({pressed}) =>
[
containerStyle,
Expand Down
6 changes: 6 additions & 0 deletions src/components/PopoverMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {

/** Should we apply padding style in modal itself. If this value is false, we will handle it in ScreenWrapper */
shouldUseModalPaddingStyle?: boolean;

/** Used to locate the component in the tests */
testID?: string;
};

const renderWithConditionalWrapper = (shouldUseScrollView: boolean, contentContainerStyle: StyleProp<ViewStyle>, children: ReactNode): React.JSX.Element => {
Expand Down Expand Up @@ -174,6 +177,7 @@ function PopoverMenu({
shouldUseScrollView = false,
shouldUpdateFocusedIndex = true,
shouldUseModalPaddingStyle,
testID,
}: PopoverMenuProps) {
const styles = useThemeStyles();
const theme = useTheme();
Expand Down Expand Up @@ -261,6 +265,7 @@ function PopoverMenu({
<FocusableMenuItem
// eslint-disable-next-line react/no-array-index-key
key={`${item.text}_${menuIndex}`}
pressableTestID={`PopoverMenuItem-${item.text}`}
title={text}
onPress={() => selectItem(menuIndex)}
focused={focusedIndex === menuIndex}
Expand Down Expand Up @@ -357,6 +362,7 @@ function PopoverMenu({
restoreFocusType={restoreFocusType}
innerContainerStyle={innerContainerStyle}
shouldUseModalPaddingStyle={shouldUseModalPaddingStyle}
testID={testID}
>
<FocusTrapForModal active={isVisible}>
<View style={[isSmallScreenWidth ? {maxHeight: windowHeight - 250} : styles.createMenuContainer, containerStyles]}>
Expand Down
1 change: 1 addition & 0 deletions src/components/SelectionList/TableListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ function TableListItem<TItem extends ListItem>({
// eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing
disabled={isDisabled || item.isDisabledCheckbox}
onPress={handleCheckboxPress}
testID={`TableListItemCheckbox-${item.text}`}
style={[styles.cursorUnset, StyleUtils.getCheckboxPressableStyle(), item.isDisabledCheckbox && styles.cursorDisabled, styles.mr3, item.cursorStyle]}
>
<View style={[StyleUtils.getCheckboxContainerStyle(20), StyleUtils.getMultiselectListStyles(!!item.isSelected, !!item.isDisabled), item.cursorStyle]}>
Expand Down
41 changes: 24 additions & 17 deletions src/pages/workspace/categories/WorkspaceCategoriesPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
const [deleteCategoriesConfirmModalVisible, setDeleteCategoriesConfirmModalVisible] = useState(false);
const isFocused = useIsFocused();
const {environmentURL} = useEnvironment();
const policyId = route.params.policyID ?? '-1';
const policyId = route.params.policyID;
const backTo = route.params?.backTo;
const policy = usePolicy(policyId);
const {selectionMode} = useMobileSelectionMode();
Expand Down Expand Up @@ -105,22 +105,28 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
setSelectedCategories({});
}, [isFocused]);

const categoryList = useMemo<PolicyOption[]>(
() =>
(lodashSortBy(Object.values(policyCategories ?? {}), 'name', localeCompare) as PolicyCategory[]).map((value) => {
const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;
return {
text: value.name,
keyForList: value.name,
isSelected: !!selectedCategories[value.name] && canSelectMultiple,
isDisabled,
pendingAction: value.pendingAction,
errors: value.errors ?? undefined,
rightElement: <ListItemRightCaretWithLabel labelText={value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} />,
};
}),
[policyCategories, selectedCategories, canSelectMultiple, translate],
);
const categoryList = useMemo<PolicyOption[]>(() => {
const categories = lodashSortBy(Object.values(policyCategories ?? {}), 'name', localeCompare) as PolicyCategory[];
return categories.reduce<PolicyOption[]>((acc, value) => {
const isDisabled = value.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE;

if (!isOffline && isDisabled) {
return acc;
}

acc.push({
text: value.name,
keyForList: value.name,
isSelected: !!selectedCategories[value.name] && canSelectMultiple,
isDisabled,
pendingAction: value.pendingAction,
errors: value.errors ?? undefined,
rightElement: <ListItemRightCaretWithLabel labelText={value.enabled ? translate('workspace.common.enabled') : translate('workspace.common.disabled')} />,
});

return acc;
}, []);
}, [policyCategories, isOffline, selectedCategories, canSelectMultiple, translate]);

useAutoTurnSelectionModeOffWhenHasNoActiveOption(categoryList);

Expand Down Expand Up @@ -248,6 +254,7 @@ function WorkspaceCategoriesPage({route}: WorkspaceCategoriesPageProps) {
isSplitButton={false}
style={[shouldUseNarrowLayout && styles.flexGrow1, shouldUseNarrowLayout && styles.mb3]}
isDisabled={!selectedCategoriesArray.length}
testID={`${WorkspaceCategoriesPage.displayName}-header-dropdown-menu-button`}
/>
);
}
Expand Down
174 changes: 174 additions & 0 deletions tests/ui/WorkspaceCategoriesTest.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
import {PortalProvider} from '@gorhom/portal';
import {NavigationContainer} from '@react-navigation/native';
import {act, fireEvent, render, screen, waitFor} from '@testing-library/react-native';
import React from 'react';
import Onyx from 'react-native-onyx';
import ComposeProviders from '@components/ComposeProviders';
import {LocaleContextProvider} from '@components/LocaleContextProvider';
import OnyxProvider from '@components/OnyxProvider';
import {CurrentReportIDContextProvider} from '@components/withCurrentReportID';
import * as useResponsiveLayoutModule from '@hooks/useResponsiveLayout';
import type ResponsiveLayoutResult from '@hooks/useResponsiveLayout/types';
import * as Localize from '@libs/Localize';
import createResponsiveStackNavigator from '@navigation/AppNavigator/createResponsiveStackNavigator';
import type {FullScreenNavigatorParamList} from '@navigation/types';
import WorkspaceCategoriesPage from '@pages/workspace/categories/WorkspaceCategoriesPage';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import SCREENS from '@src/SCREENS';
import * as LHNTestUtils from '../utils/LHNTestUtils';
import * as TestHelper from '../utils/TestHelper';
import waitForBatchedUpdatesWithAct from '../utils/waitForBatchedUpdatesWithAct';

TestHelper.setupGlobalFetchMock();

const RootStack = createResponsiveStackNavigator<FullScreenNavigatorParamList>();

const renderPage = (initialRouteName: typeof SCREENS.WORKSPACE.CATEGORIES, initialParams: FullScreenNavigatorParamList[typeof SCREENS.WORKSPACE.CATEGORIES]) => {
return render(
<ComposeProviders components={[OnyxProvider, LocaleContextProvider, CurrentReportIDContextProvider]}>
<PortalProvider>
<NavigationContainer>
<RootStack.Navigator initialRouteName={initialRouteName}>
<RootStack.Screen
name={SCREENS.WORKSPACE.CATEGORIES}
component={WorkspaceCategoriesPage}
initialParams={initialParams}
/>
</RootStack.Navigator>
</NavigationContainer>
</PortalProvider>
</ComposeProviders>,
);
};

describe('WorkspaceCategories', () => {
const FIRST_CATEGORY = 'categoryOne';
const SECOND_CATEGORY = 'categoryTwo';

beforeAll(() => {
Onyx.init({
keys: ONYXKEYS,
});
});

beforeEach(() => {
jest.spyOn(useResponsiveLayoutModule, 'default').mockReturnValue({
isSmallScreenWidth: false,
shouldUseNarrowLayout: false,
} as ResponsiveLayoutResult);
});

afterEach(async () => {
await act(async () => {
await Onyx.clear();
});
jest.clearAllMocks();
});

it('should delete categories through UI interactions', async () => {
await TestHelper.signInWithTestUser();

const policy = {
...LHNTestUtils.getFakePolicy(),
role: CONST.POLICY.ROLE.ADMIN,
areCategoriesEnabled: true,
};

const categories = {
[FIRST_CATEGORY]: {
name: FIRST_CATEGORY,
enabled: true,
},
[SECOND_CATEGORY]: {
name: SECOND_CATEGORY,
enabled: true,
},
};

// Initialize categories
await act(async () => {
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policy.id}`, policy);
await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_CATEGORIES}${policy.id}`, categories);
});

const {unmount} = renderPage(SCREENS.WORKSPACE.CATEGORIES, {policyID: policy.id});

await waitForBatchedUpdatesWithAct();

// Wait for initial render and verify categories are visible
await waitFor(() => {
expect(screen.getByText(FIRST_CATEGORY)).toBeOnTheScreen();
});
await waitFor(() => {
expect(screen.getByText(SECOND_CATEGORY)).toBeOnTheScreen();
});

// Select categories to delete by clicking their checkboxes
fireEvent.press(screen.getByTestId(`TableListItemCheckbox-${FIRST_CATEGORY}`));
fireEvent.press(screen.getByTestId(`TableListItemCheckbox-${SECOND_CATEGORY}`));

const dropdownMenuButtonTestID = `${WorkspaceCategoriesPage.displayName}-header-dropdown-menu-button`;

// Wait for selection mode to be active and click the dropdown menu button
await waitFor(() => {
expect(screen.getByTestId(dropdownMenuButtonTestID)).toBeOnTheScreen();
});

// Click the "2 selected" button to open the menu
const dropdownButton = screen.getByTestId(dropdownMenuButtonTestID);
fireEvent.press(dropdownButton);

await waitForBatchedUpdatesWithAct();

// Wait for menu items to be visible
await waitFor(() => {
const deleteText = Localize.translateLocal('workspace.categories.deleteCategories');
expect(screen.getByText(deleteText)).toBeOnTheScreen();
});

// Find and verify "Delete categories" dropdown menu item
const deleteMenuItem = screen.getByTestId('PopoverMenuItem-Delete categories');
expect(deleteMenuItem).toBeOnTheScreen();

// Create a mock event object that matches GestureResponderEvent. Needed for onPress in MenuItem to be called
const mockEvent = {
nativeEvent: {},
type: 'press',
target: deleteMenuItem,
currentTarget: deleteMenuItem,
};
fireEvent.press(deleteMenuItem, mockEvent);

await waitForBatchedUpdatesWithAct();

// After clicking delete categories dropdown menu item, verify the confirmation modal appears
await waitFor(() => {
const confirmModalPrompt = Localize.translateLocal('workspace.categories.deleteCategoriesPrompt');
expect(screen.getByText(confirmModalPrompt)).toBeOnTheScreen();
});

// Verify the delete button in the modal is visible
await waitFor(() => {
const deleteConfirmButton = screen.getByLabelText(Localize.translateLocal('common.delete'));
expect(deleteConfirmButton).toBeOnTheScreen();
});

// Click the delete button in the confirmation modal
const deleteConfirmButton = screen.getByLabelText(Localize.translateLocal('common.delete'));
fireEvent.press(deleteConfirmButton);

await waitForBatchedUpdatesWithAct();

// Verify the categories are deleted from the UI
await waitFor(() => {
expect(screen.queryByText(FIRST_CATEGORY)).not.toBeOnTheScreen();
});
await waitFor(() => {
expect(screen.queryByText(SECOND_CATEGORY)).not.toBeOnTheScreen();
});

unmount();
await waitForBatchedUpdatesWithAct();
});
});
Loading