Skip to content

Commit 9d0898c

Browse files
chore: update with master
2 parents af2b4dd + 1e7e3e7 commit 9d0898c

28 files changed

+4063
-5323
lines changed

.github/workflows/lockfileversion-check.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,4 +10,4 @@ on:
1010

1111
jobs:
1212
version-check:
13-
uses: openedx/.github/.github/workflows/lockfile-check.yml@master
13+
uses: openedx/.github/.github/workflows/lockfileversion-check-v3.yml@master

.github/workflows/validate.yml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@ jobs:
1212
strategy:
1313
matrix:
1414
node: [18, 20]
15-
continue-on-error: ${{ matrix.node == 20 }}
1615

1716
steps:
1817
- uses: actions/checkout@v4

.nvmrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
18
1+
20

package-lock.json

Lines changed: 2964 additions & 5143 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/library-authoring/LibraryAuthoringPage.scss

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,9 @@
99
}
1010
}
1111
}
12+
13+
.library-authoring-sidebar {
14+
min-width: 300px;
15+
max-width: map-get($grid-breakpoints, "sm");
16+
z-index: 1001; // to appear over header
17+
}

src/library-authoring/LibraryAuthoringPage.test.tsx

Lines changed: 161 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,14 @@ import {
1010
render,
1111
waitFor,
1212
screen,
13+
within,
1314
} from '@testing-library/react';
1415
import fetchMock from 'fetch-mock-jest';
1516
import initializeStore from '../store';
1617
import { getContentSearchConfigUrl } from '../search-manager/data/api';
1718
import mockResult from '../search-modal/__mocks__/search-result.json';
1819
import mockEmptyResult from '../search-modal/__mocks__/empty-search-result.json';
19-
import { getContentLibraryApiUrl, type ContentLibrary } from './data/api';
20+
import { getContentLibraryApiUrl, getXBlockFieldsApiUrl, type ContentLibrary } from './data/api';
2021
import { LibraryLayout } from '.';
2122

2223
let store;
@@ -61,16 +62,17 @@ const returnEmptyResult = (_url, req) => {
6162
const returnLowNumberResults = (_url, req) => {
6263
const requestData = JSON.parse(req.body?.toString() ?? '');
6364
const query = requestData?.queries[0]?.q ?? '';
65+
const newMockResult = { ...mockResult };
6466
// We have to replace the query (search keywords) in the mock results with the actual query,
6567
// because otherwise we may have an inconsistent state that causes more queries and unexpected results.
66-
mockResult.results[0].query = query;
68+
newMockResult.results[0].query = query;
6769
// Limit number of results to just 2
68-
mockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2);
69-
mockResult.results[0].estimatedTotalHits = 2;
70+
newMockResult.results[0].hits = mockResult.results[0]?.hits.slice(0, 2);
71+
newMockResult.results[0].estimatedTotalHits = 2;
7072
// And fake the required '_formatted' fields; it contains the highlighting <mark>...</mark> around matched words
7173
// eslint-disable-next-line no-underscore-dangle, no-param-reassign
72-
mockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
73-
return mockResult;
74+
newMockResult.results[0]?.hits.forEach((hit) => { hit._formatted = { ...hit }; });
75+
return newMockResult;
7476
};
7577

7678
const libraryData: ContentLibrary = {
@@ -97,6 +99,13 @@ const libraryData: ContentLibrary = {
9799
updated: '2024-07-20',
98100
};
99101

102+
const xBlockFields = {
103+
display_name: 'Test HTML Block',
104+
metadata: {
105+
display_name: 'Test HTML Block',
106+
},
107+
};
108+
100109
const clipboardBroadcastChannelMock = {
101110
postMessage: jest.fn(),
102111
close: jest.fn(),
@@ -158,6 +167,19 @@ describe('<LibraryAuthoringPage />', () => {
158167
queryClient.clear();
159168
});
160169

170+
const renderLibraryPage = async () => {
171+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
172+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
173+
174+
const result = render(<RootWrapper />);
175+
176+
// Ensure the search endpoint is called:
177+
// Call 1: To fetch searchable/filterable/sortable library data
178+
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
179+
180+
return result;
181+
};
182+
161183
it('shows the spinner before the query is complete', () => {
162184
mockUseParams.mockReturnValue({ libraryId: '1' });
163185
// @ts-ignore Use unresolved promise to keep the Loading visible
@@ -185,12 +207,9 @@ describe('<LibraryAuthoringPage />', () => {
185207
});
186208

187209
it('show library data', async () => {
188-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
189-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
190-
191210
const {
192211
getByRole, getAllByText, getByText, queryByText, findByText, findAllByText,
193-
} = render(<RootWrapper />);
212+
} = await renderLibraryPage();
194213

195214
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
196215

@@ -263,10 +282,7 @@ describe('<LibraryAuthoringPage />', () => {
263282
});
264283

265284
it('show new content button', async () => {
266-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
267-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
268-
269-
render(<RootWrapper />);
285+
await renderLibraryPage();
270286

271287
expect(await screen.findByRole('heading')).toBeInTheDocument();
272288
expect(screen.getByRole('button', { name: /new/i })).toBeInTheDocument();
@@ -322,10 +338,7 @@ describe('<LibraryAuthoringPage />', () => {
322338
});
323339

324340
it('should open and close new content sidebar', async () => {
325-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
326-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
327-
328-
render(<RootWrapper />);
341+
await renderLibraryPage();
329342

330343
expect(await screen.findByRole('heading')).toBeInTheDocument();
331344
expect(screen.queryByText(/add content/i)).not.toBeInTheDocument();
@@ -342,10 +355,7 @@ describe('<LibraryAuthoringPage />', () => {
342355
});
343356

344357
it('should open Library Info by default', async () => {
345-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
346-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
347-
348-
render(<RootWrapper />);
358+
await renderLibraryPage();
349359

350360
expect(await screen.findByText('Content library')).toBeInTheDocument();
351361
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
@@ -361,10 +371,7 @@ describe('<LibraryAuthoringPage />', () => {
361371
});
362372

363373
it('should close and open Library Info', async () => {
364-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
365-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
366-
367-
render(<RootWrapper />);
374+
await renderLibraryPage();
368375

369376
expect(await screen.findByText('Content library')).toBeInTheDocument();
370377
expect((await screen.findAllByText(libraryData.title))[0]).toBeInTheDocument();
@@ -389,14 +396,9 @@ describe('<LibraryAuthoringPage />', () => {
389396
});
390397

391398
it('show the "View All" button when viewing library with many components', async () => {
392-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
393-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
394-
395399
const {
396400
getByRole, getByText, queryByText, getAllByText, findAllByText,
397-
} = render(<RootWrapper />);
398-
399-
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
401+
} = await renderLibraryPage();
400402

401403
expect(getByText('Content library')).toBeInTheDocument();
402404
expect((await findAllByText(libraryData.title))[0]).toBeInTheDocument();
@@ -456,13 +458,9 @@ describe('<LibraryAuthoringPage />', () => {
456458
});
457459

458460
it('sort library components', async () => {
459-
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
460-
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
461-
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
462-
463461
const {
464462
findByTitle, getAllByText, getByRole, getByTitle,
465-
} = render(<RootWrapper />);
463+
} = await renderLibraryPage();
466464

467465
expect(await findByTitle('Sort search results')).toBeInTheDocument();
468466

@@ -514,7 +512,7 @@ describe('<LibraryAuthoringPage />', () => {
514512

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

519517
// Enter a keyword into the search box
520518
const searchBox = getByRole('searchbox');
@@ -530,4 +528,129 @@ describe('<LibraryAuthoringPage />', () => {
530528
});
531529
});
532530
});
531+
532+
it('should open and close the component sidebar', async () => {
533+
const usageKey = mockResult.results[0].hits[0].usage_key;
534+
const { getAllByText, queryByTestId, queryByText } = await renderLibraryPage();
535+
axiosMock.onGet(getXBlockFieldsApiUrl(usageKey)).reply(200, xBlockFields);
536+
537+
// Click on the first component
538+
waitFor(() => expect(queryByText('Test HTML Block')).toBeInTheDocument());
539+
fireEvent.click(getAllByText('Test HTML Block')[0]);
540+
541+
const sidebar = screen.getByTestId('library-sidebar');
542+
543+
const { getByRole, getByText } = within(sidebar);
544+
545+
await waitFor(() => expect(getByText('Test HTML Block')).toBeInTheDocument());
546+
547+
const closeButton = getByRole('button', { name: /close/i });
548+
fireEvent.click(closeButton);
549+
550+
await waitFor(() => expect(queryByTestId('library-sidebar')).not.toBeInTheDocument());
551+
});
552+
553+
it('filter by capa problem type', async () => {
554+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
555+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
556+
557+
const problemTypes = {
558+
'Multiple Choice': 'choiceresponse',
559+
Checkboxes: 'multiplechoiceresponse',
560+
'Numerical Input': 'numericalresponse',
561+
Dropdown: 'optionresponse',
562+
'Text Input': 'stringresponse',
563+
};
564+
565+
render(<RootWrapper />);
566+
567+
// Ensure the search endpoint is called
568+
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
569+
const filterButton = screen.getByRole('button', { name: /type/i });
570+
fireEvent.click(filterButton);
571+
572+
const openProblemItem = screen.getByTestId('open-problem-item-button');
573+
fireEvent.click(openProblemItem);
574+
575+
const validateSubmenu = async (submenuText : string) => {
576+
const submenu = screen.getByText(submenuText);
577+
expect(submenu).toBeInTheDocument();
578+
fireEvent.click(submenu);
579+
580+
await waitFor(() => {
581+
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
582+
body: expect.stringContaining(`content.problem_types = ${problemTypes[submenuText]}`),
583+
method: 'POST',
584+
headers: expect.anything(),
585+
});
586+
});
587+
588+
fireEvent.click(submenu);
589+
await waitFor(() => {
590+
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
591+
body: expect.not.stringContaining(`content.problem_types = ${problemTypes[submenuText]}`),
592+
method: 'POST',
593+
headers: expect.anything(),
594+
});
595+
});
596+
};
597+
598+
// Validate per submenu
599+
// eslint-disable-next-line no-restricted-syntax
600+
for (const key of Object.keys(problemTypes)) {
601+
// eslint-disable-next-line no-await-in-loop
602+
await validateSubmenu(key);
603+
}
604+
605+
// Validate click on Problem type
606+
const problemMenu = screen.getByText('Problem');
607+
expect(problemMenu).toBeInTheDocument();
608+
fireEvent.click(problemMenu);
609+
await waitFor(() => {
610+
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
611+
body: expect.stringContaining('block_type = problem'),
612+
method: 'POST',
613+
headers: expect.anything(),
614+
});
615+
});
616+
617+
fireEvent.click(problemMenu);
618+
await waitFor(() => {
619+
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
620+
body: expect.not.stringContaining('block_type = problem'),
621+
method: 'POST',
622+
headers: expect.anything(),
623+
});
624+
});
625+
626+
// Validate clear filters
627+
const submenu = screen.getByText('Checkboxes');
628+
expect(submenu).toBeInTheDocument();
629+
fireEvent.click(submenu);
630+
631+
const clearFitlersButton = screen.getByRole('button', { name: /clear filters/i });
632+
fireEvent.click(clearFitlersButton);
633+
await waitFor(() => {
634+
expect(fetchMock).toHaveBeenLastCalledWith(searchEndpoint, {
635+
body: expect.not.stringContaining(`content.problem_types = ${problemTypes.Checkboxes}`),
636+
method: 'POST',
637+
headers: expect.anything(),
638+
});
639+
});
640+
});
641+
642+
it('empty type filter', async () => {
643+
mockUseParams.mockReturnValue({ libraryId: libraryData.id });
644+
axiosMock.onGet(getContentLibraryApiUrl(libraryData.id)).reply(200, libraryData);
645+
fetchMock.post(searchEndpoint, returnEmptyResult, { overwriteRoutes: true });
646+
647+
render(<RootWrapper />);
648+
649+
await waitFor(() => { expect(fetchMock).toHaveFetchedTimes(1, searchEndpoint, 'post'); });
650+
651+
const filterButton = screen.getByRole('button', { name: /type/i });
652+
fireEvent.click(filterButton);
653+
654+
expect(screen.getByText(/no matching components/i)).toBeInTheDocument();
655+
});
533656
});

0 commit comments

Comments
 (0)