Skip to content

Commit

Permalink
feat: unify library component and collections listing
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielVZ96 committed Nov 3, 2024
1 parent 59815e5 commit d9be05d
Show file tree
Hide file tree
Showing 24 changed files with 311 additions and 831 deletions.
140 changes: 13 additions & 127 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,40 +40,12 @@ const returnEmptyResult = (_url, req) => {
// We have to replace the query (search keywords) in the mock results with the actual query,
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
mockEmptyResult.results[0].query = query;
mockEmptyResult.results[2].query = query;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
mockEmptyResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
mockEmptyResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return mockEmptyResult;
};

/**
* Returns 2 components from the search query.
* This lets us test that the StudioHome "View All" button is hidden when a
* low number of search results are shown (<=4 by default).
*/
const returnLowNumberResults = (_url, req) => {
const requestData = JSON.parse(req.body?.toString() ?? '');
const query = requestData?.queries[0]?.q ?? '';
const newMockResult = { ...mockResult };
// We have to replace the query (search keywords) in the mock results with the actual query,
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
newMockResult.results[0].query = query;
// Limit number of results to just 2
newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2);
newMockResult.results[2].hits = mockResult.results[2]?.hits.slice(0, 2);
newMockResult.results[0].estimatedTotalHits = 2;
newMockResult.results[2].estimatedTotalHits = 2;
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
newMockResult.results[2]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
return newMockResult;
};

const path = '/library/:libraryId/*';
const libraryTitle = mockContentLibrary.libraryData.title;

Expand Down Expand Up @@ -133,35 +105,25 @@ describe('<LibraryAuthoringPage />', () => {

expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();

// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect((await screen.findAllByText('Introduction to Testing'))[0]).toBeInTheDocument();

// Navigate to the components tab
fireEvent.click(screen.getByRole('tab', { name: 'Components' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();

// Navigate to the collections tab
fireEvent.click(screen.getByRole('tab', { name: 'Collections' }));
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.queryByText('There are 10 components in this library')).not.toBeInTheDocument();
expect((await screen.findAllByText('Collection 1'))[0]).toBeInTheDocument();

// Go back to Home tab
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
});

it('shows a library without components and collections', async () => {
Expand All @@ -185,7 +147,7 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(cancelButton);
expect(collectionModalHeading).not.toBeInTheDocument();

fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();

const addComponentButton = screen.getByRole('button', { name: /add component/i });
Expand Down Expand Up @@ -243,7 +205,7 @@ describe('<LibraryAuthoringPage />', () => {

// Go back to Home tab
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
fireEvent.click(screen.getByRole('tab', { name: 'All Content' }));
});

it('should open and close new content sidebar', async () => {
Expand Down Expand Up @@ -325,68 +287,6 @@ describe('<LibraryAuthoringPage />', () => {
expect(manageAccess).not.toBeInTheDocument();
});

it('show the "View All" button when viewing library with many components', async () => {
await renderLibraryPage();

expect(screen.getByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();

// "Recently Modified" header + sort shown
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();

// There should be two "View All" button, since the Components and Collections count
// are above the preview limit (4)
expect(screen.getAllByText('View All').length).toEqual(2);

// Clicking on first "View All" button should navigate to the Collections tab
fireEvent.click(screen.getAllByText('View All')[0]);
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.getByText('Collection 1')).toBeInTheDocument();

fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
// Clicking on second "View All" button should navigate to the Components tab
fireEvent.click(screen.getAllByText('View All')[1]);
// "Recently Modified" default sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(1);
expect(screen.queryByText('Collections (6)')).not.toBeInTheDocument();
expect(screen.queryByText('Components (10)')).not.toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();

// Go back to Home tab
// This step is necessary to avoid the url change leak to other tests
fireEvent.click(screen.getByRole('tab', { name: 'Home' }));
// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
expect(screen.getByText('Collections (6)')).toBeInTheDocument();
expect(screen.getByText('Components (10)')).toBeInTheDocument();
});

it('should not show the "View All" button when viewing library with low number of components', async () => {
fetchMock.post(searchEndpoint, returnLowNumberResults, { overwriteRoutes: true });
await renderLibraryPage();

expect(screen.getByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();

// "Recently Modified" header + sort shown
await waitFor(() => { expect(screen.getAllByText('Recently Modified').length).toEqual(2); });
expect(screen.getByText('Collections (2)')).toBeInTheDocument();
expect(screen.getByText('Components (2)')).toBeInTheDocument();
expect(screen.getAllByText('Introduction to Testing')[0]).toBeInTheDocument();
expect(screen.queryByText('You have not added any content to this library yet.')).not.toBeInTheDocument();

// There should not be any "View All" button on page since Components count
// is less than the preview limit (4)
expect(screen.queryByText('View All')).not.toBeInTheDocument();
});

it('sorts library components', async () => {
await renderLibraryPage();

Expand Down Expand Up @@ -441,7 +341,7 @@ describe('<LibraryAuthoringPage />', () => {

// Re-selecting the previous sort option resets sort to default "Recently Modified"
await testSortOption('Recently Published', 'modified:desc', true);
expect(screen.getAllByText('Recently Modified').length).toEqual(3);
expect(screen.getAllByText('Recently Modified').length).toEqual(2);

// Enter a keyword into the search box
const searchBox = screen.getByRole('searchbox');
Expand All @@ -464,7 +364,6 @@ describe('<LibraryAuthoringPage />', () => {
expect(mockResult0.display_name).toStrictEqual(displayName);
await renderLibraryPage();

// Click on the first component. It should appear twice, in both "Recently Modified" and "Components"
fireEvent.click((await screen.findAllByText(displayName))[0]);

const sidebar = screen.getByTestId('library-sidebar');
Expand Down Expand Up @@ -576,7 +475,7 @@ describe('<LibraryAuthoringPage />', () => {
}

// Validate click on Problem type
const problemMenu = screen.getByText('Problem');
const problemMenu = screen.getAllByText('Problem')[0];
expect(problemMenu).toBeInTheDocument();
fireEvent.click(problemMenu);
await waitFor(() => {
Expand Down Expand Up @@ -644,7 +543,8 @@ describe('<LibraryAuthoringPage />', () => {
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
Expand Down Expand Up @@ -688,7 +588,8 @@ describe('<LibraryAuthoringPage />', () => {
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
Expand Down Expand Up @@ -721,7 +622,8 @@ describe('<LibraryAuthoringPage />', () => {
expect(screen.getByText(/add content/i)).toBeInTheDocument();

// Open New collection Modal
const newCollectionButton = screen.getAllByRole('button', { name: /collection/i })[4];
const sidebar = screen.getByTestId('library-sidebar');
const newCollectionButton = within(sidebar).getAllByRole('button', { name: /collection/i })[0];
fireEvent.click(newCollectionButton);
const collectionModalHeading = await screen.findByRole('heading', { name: /new collection/i });
expect(collectionModalHeading).toBeInTheDocument();
Expand All @@ -736,22 +638,6 @@ describe('<LibraryAuthoringPage />', () => {
fireEvent.click(createButton);
});

it('shows both components and collections in recently modified section', async () => {
await renderLibraryPage();

expect(await screen.findByText('Content library')).toBeInTheDocument();
expect((await screen.findAllByText(libraryTitle))[0]).toBeInTheDocument();

// "Recently Modified" header + sort shown
expect(screen.getAllByText('Recently Modified').length).toEqual(2);
const recentModifiedContainer = (await screen.findAllByText('Recently Modified'))[1].parentElement?.parentElement?.parentElement;
expect(recentModifiedContainer).toBeTruthy();

const container = within(recentModifiedContainer!);
expect(container.queryAllByText('Text').length).toBeGreaterThan(0);
expect(container.queryAllByText('Collection').length).toBeGreaterThan(0);
});

it('shows a single block when usageKey query param is set', async () => {
render(<LibraryLayout />, {
path,
Expand Down
28 changes: 7 additions & 21 deletions src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,7 @@ import {
SearchKeywordsField,
SearchSortWidget,
} from '../search-manager';
import LibraryComponents from './components/LibraryComponents';
import LibraryCollections from './collections/LibraryCollections';
import LibraryHome from './LibraryHome';
import LibraryContent from './LibraryContent';
import { LibrarySidebar } from './library-sidebar';
import { SidebarBodyComponentId, useLibraryContext } from './common/context';
import messages from './messages';
Expand All @@ -46,21 +44,6 @@ enum TabList {
collections = 'collections',
}

interface TabContentProps {
eventKey: string;
}

const TabContent = ({ eventKey }: TabContentProps) => {
switch (eventKey) {
case TabList.components:
return <LibraryComponents />;
case TabList.collections:
return <LibraryCollections />;
default:
return <LibraryHome />;
}
};

const HeaderActions = () => {
const intl = useIntl();
const {
Expand Down Expand Up @@ -218,9 +201,12 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
extraFilter.push('last_published IS NOT NULL');
}

const activeTypeFilters = {
components: 'NOT type = "collection"',
collections: 'type = "collection"',
};
if (activeKey !== TabList.home) {
const activeType = activeKey === 'components' ? 'component' : 'collection';
extraFilter.push(`type = "${activeType}"`);
extraFilter.push(activeTypeFilters[activeKey]);
}

return (
Expand Down Expand Up @@ -267,7 +253,7 @@ const LibraryAuthoringPage = ({ returnToLibrarySelection }: LibraryAuthoringPage
<Tab eventKey={TabList.components} title={intl.formatMessage(messages.componentsTab)} />
<Tab eventKey={TabList.collections} title={intl.formatMessage(messages.collectionsTab)} />
</Tabs>
<TabContent eventKey={activeKey} />
<LibraryContent content={activeKey} />
</SearchContextProvider>
</Container>
{!componentPickerMode && <StudioFooter containerProps={{ size: undefined }} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import fetchMock from 'fetch-mock-jest';

import {
fireEvent,
render,
screen,
initializeMocks,
Expand All @@ -9,7 +10,8 @@ import { getContentSearchConfigUrl } from '../search-manager/data/api';
import { mockContentLibrary } from './data/api.mocks';
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
import { LibraryProvider } from './common/context';
import LibraryHome from './LibraryHome';
import LibraryContent from './LibraryContent';
import { libraryComponentsMock } from './__mocks__';

const searchEndpoint = 'http://mock.meilisearch.local/multi-search';

Expand Down Expand Up @@ -83,27 +85,31 @@ describe('<LibraryHome />', () => {
isLoading: true,
});

render(<LibraryHome />, withLibraryId(mockContentLibrary.libraryId));
render(<LibraryContent content="" />, withLibraryId(mockContentLibrary.libraryId));
expect(screen.getByText('Loading...')).toBeInTheDocument();
});

it('should render a load more button when there are more pages', async () => {
it('should render an empty state when there are no results', async () => {
mockUseSearchContext.mockReturnValue({
...data,
totalContentAndCollectionHits: 1,
hasNextPage: true,
totalHits: 0,
});

render(<LibraryHome />, withLibraryId(mockContentLibrary.libraryId));
expect(screen.getByText('Show more results')).toBeInTheDocument();
render(<LibraryContent content="" />, withLibraryId(mockContentLibrary.libraryId));
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
});

it('should render an empty state when there are no results', async () => {
it('should load more results when the user scrolls to the bottom', async () => {
mockUseSearchContext.mockReturnValue({
...data,
totalHits: 0,
hits: libraryComponentsMock,
hasNextPage: true,
});
render(<LibraryHome />, withLibraryId(mockContentLibrary.libraryId));
expect(screen.getByText('You have not added any content to this library yet.')).toBeInTheDocument();
render(<LibraryContent content="" />, withLibraryId(mockContentLibrary.libraryId));

Object.defineProperty(window, 'innerHeight', { value: 800 });
Object.defineProperty(document.body, 'scrollHeight', { value: 1600 });

fireEvent.scroll(window, { target: { scrollY: 1000 } });
expect(mockFetchNextPage).toHaveBeenCalled();
});
});
Loading

0 comments on commit d9be05d

Please sign in to comment.