Skip to content
Open
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
35 changes: 28 additions & 7 deletions src/library-authoring/collections/LibraryCollectionPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('<LibraryCollectionPage />', () => {
const mocks = initializeMocks();
axiosMock = mocks.axiosMock;
mockShowToast = mocks.mockShowToast;
jest.useFakeTimers();
fetchMock.mockReset();

// The Meilisearch client-side API uses fetch, not Axios.
Expand Down Expand Up @@ -89,6 +90,10 @@ describe('<LibraryCollectionPage />', () => {
});
});

afterEach(() => {
jest.runOnlyPendingTimers();
jest.useRealTimers();
});
const renderLibraryCollectionPage = async (collectionId?: string, libraryId?: string) => {
const libId = libraryId || mockContentLibrary.libraryId;
const colId = collectionId || mockCollection.collectionId;
Expand Down Expand Up @@ -354,28 +359,40 @@ describe('<LibraryCollectionPage />', () => {
expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
});

it('should remove component from collection and hides sidebar', async () => {
it('should remove unit from collection and hides sidebar', async () => {
const url = getLibraryCollectionItemsApiUrl(
mockContentLibrary.libraryId,
mockCollection.collectionId,
);
axiosMock.onDelete(url).reply(204);
const displayName = 'Introduction to Testing';
const displayName = 'Test Unit';
await renderLibraryCollectionPage();

// Wait for the unit cards to load
await waitFor(() => expect(screen.getAllByTestId('container-card-menu-toggle').length).toBeGreaterThan(0));

// open sidebar
fireEvent.click(await screen.findByText(displayName));

// advance timers so the sidebar opens
jest.advanceTimersByTime(500);

await waitFor(() => expect(screen.queryByTestId('library-sidebar')).toBeInTheDocument());

const menuBtns = await screen.findAllByRole('button', { name: 'Component actions menu' });
// open menu
fireEvent.click(menuBtns[0]);
// Open menu
fireEvent.click((await screen.findAllByTestId('container-card-menu-toggle'))[0]);

// Click remove to collection
fireEvent.click(screen.getByRole('button', { name: 'Remove from collection' }));

fireEvent.click(await screen.findByText('Remove from collection'));
await waitFor(() => {
expect(axiosMock.history.delete.length).toEqual(1);
});
expect(mockShowToast).toHaveBeenCalledWith('Item successfully removed');

// advance timers so the sidebar close logic executes
jest.advanceTimersByTime(500);

// Should close sidebar as component was removed
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
Expand All @@ -399,7 +416,7 @@ describe('<LibraryCollectionPage />', () => {
expect(mockShowToast).toHaveBeenCalledWith('Failed to remove item');
});

it('should remove unit from collection and hides sidebar', async () => {
it.only('should remove unit from collection and hides sidebar', async () => {
const url = getLibraryCollectionItemsApiUrl(
mockContentLibrary.libraryId,
mockCollection.collectionId,
Expand All @@ -413,6 +430,8 @@ describe('<LibraryCollectionPage />', () => {

// open sidebar
fireEvent.click(await screen.findByText(displayName));
// ⏩ let the 500ms pass in test-land
jest.advanceTimersByTime(500);
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).toBeInTheDocument());

// Open menu
Expand All @@ -425,6 +444,8 @@ describe('<LibraryCollectionPage />', () => {
expect(axiosMock.history.delete.length).toEqual(1);
});
expect(mockShowToast).toHaveBeenCalledWith('Item successfully removed');
// ⏩ let the 500ms pass in test-land
jest.advanceTimersByTime(500);
// Should close sidebar as component was removed
await waitFor(() => expect(screen.queryByTestId('library-sidebar')).not.toBeInTheDocument());
});
Expand Down
21 changes: 21 additions & 0 deletions src/library-authoring/containers/ContainerCard.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,27 @@ describe('<ContainerCard />', () => {
expect(mockShowToast).toHaveBeenCalledWith('Failed to delete unit');
});

it('should clear click timer on unmount to avoid running sidebar open after unmount', () => {
jest.useFakeTimers();
const container = getContainerHitSample();
const { unmount } = render(<ContainerCard hit={container} />);

// Single click -> starts the setTimeout
fireEvent.click(screen.getByText('unit Display Formated Name'));

// Unmount before the timeout fires
unmount();

// Fast-forward timers
jest.advanceTimersByTime(500);

// Expect nothing got called after unmount
expect(mockNavigate).not.toHaveBeenCalled();
expect(mockShowToast).not.toHaveBeenCalled();

jest.useRealTimers();
});

it('should render no child blocks in unit card preview', async () => {
render(<ContainerCard hit={getContainerHitSample()} />);

Expand Down
45 changes: 33 additions & 12 deletions src/library-authoring/containers/ContainerCard.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { ReactNode, useCallback, useContext } from 'react';
import {
ReactNode, useCallback, useContext, useRef, useEffect,
} from 'react';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import {
ActionRow,
Expand Down Expand Up @@ -26,6 +28,8 @@ import { useRunOnNextRender } from '../../utils';
import BaseCard from '../components/BaseCard';
import AddComponentWidget from '../components/AddComponentWidget';

const DOUBLE_CLICK_DELAY = 500; // ms

type ContainerMenuProps = {
containerKey: string;
displayName: string;
Expand Down Expand Up @@ -247,6 +251,8 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {
const { showOnlyPublished } = useLibraryContext();
const { openContainerInfoSidebar, openItemSidebar, sidebarItemInfo } = useSidebarContext();

const clickTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);

const {
blockType: itemType,
formatted,
Expand All @@ -269,18 +275,33 @@ const ContainerCard = ({ hit } : ContainerCardProps) => {

const { navigateTo } = useLibraryRoutes();

const selectContainer = useCallback((e?: React.MouseEvent) => {
const doubleClicked = (e?.detail || 0) > 1;
if (componentPickerMode) {
// In component picker mode, we want to open the sidebar
// without changing the URL
openContainerInfoSidebar(containerKey);
} else if (!doubleClicked) {
openItemSidebar(containerKey, SidebarBodyItemId.ContainerInfo);
} else {
navigateTo({ containerId: containerKey });
const selectContainer = useCallback(
() => {
if (clickTimerRef.current) {
clearTimeout(clickTimerRef.current);
clickTimerRef.current = null;

navigateTo({ containerId: containerKey });
} else {
clickTimerRef.current = setTimeout(() => {
clickTimerRef.current = null;

if (componentPickerMode) {
openContainerInfoSidebar(containerKey);
} else {
openItemSidebar(containerKey, SidebarBodyItemId.ContainerInfo);
}
}, DOUBLE_CLICK_DELAY);
}
},
[containerKey, componentPickerMode, openContainerInfoSidebar, openItemSidebar, navigateTo],
);

useEffect(() => () => {
if (clickTimerRef.current) {
clearTimeout(clickTimerRef.current);
}
}, [containerKey, openContainerInfoSidebar, openItemSidebar, navigateTo]);
}, []);

return (
<BaseCard
Expand Down