Skip to content

Commit

Permalink
feat: create library (v2) form (#1116)
Browse files Browse the repository at this point in the history
  • Loading branch information
rpenido authored Jul 12, 2024
1 parent cc41a2f commit e087001
Show file tree
Hide file tree
Showing 27 changed files with 570 additions and 185 deletions.
5 changes: 5 additions & 0 deletions src/constants.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,8 @@ export const CLIPBOARD_STATUS = {
};

export const STRUCTURAL_XBLOCK_TYPES = ['vertical', 'sequential', 'chapter', 'course'];

export const REGEX_RULES = {
specialCharsRule: /^[a-zA-Z0-9_\-.'*~\s]+$/,
noSpaceRule: /^\S*$/,
};
40 changes: 40 additions & 0 deletions src/generic/alert-error/AlertError.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';

import AlertError from '.';

const RootWrapper = ({ error }: { error: unknown }) => (
<IntlProvider locale="en">
<AlertError error={error} />
</IntlProvider>
);

describe('<AlertMessage />', () => {
test('render using a string', () => {
const error = 'This is a string error message';
const { getByText } = render(<RootWrapper error={error} />);
expect(getByText('This is a string error message')).toBeInTheDocument();
});

test('render using an error', () => {
const error = new Error('This is an error message');
const { getByText } = render(<RootWrapper error={error} />);
expect(getByText('This is an error message')).toBeInTheDocument();
});

test('render using an error with response', () => {
const error = {
message: 'This is an error message',
response: {
data: {
message: 'This is a response body',
},
},
};
const { getByText } = render(<RootWrapper error={error} />);
screen.logTestingPlaygroundURL();
expect(getByText(/this is an error message/i)).toBeInTheDocument();
expect(getByText(/\{"message":"this is a response body"\}/i)).toBeInTheDocument();
});
});
14 changes: 14 additions & 0 deletions src/generic/alert-error/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import React from 'react';
import {
Alert,
} from '@openedx/paragon';

const AlertError: React.FC<{ error: unknown }> = ({ error }) => (
<Alert variant="danger" className="mt-3">
{error instanceof Object && 'message' in error ? error.message : String(error)}
<br />
{error instanceof Object && (error as any).response?.data && JSON.stringify((error as any).response?.data)}
</Alert>
);

export default AlertError;
5 changes: 3 additions & 2 deletions src/generic/create-or-rerun-course/hooks.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { useFormik } from 'formik';
import * as Yup from 'yup';
import { useNavigate } from 'react-router-dom';

import { REGEX_RULES } from '../../constants';
import { RequestStatus } from '../../data/constants';
import { getStudioHomeData } from '../../studio-home/data/selectors';
import {
Expand Down Expand Up @@ -32,8 +33,8 @@ const useCreateOrRerunCourse = (initialValues) => {
const [isFormFilled, setFormFilled] = useState(false);
const [showErrorBanner, setShowErrorBanner] = useState(false);
const organizations = allowToCreateNewOrg ? allOrganizations : allowedOrganizations;
const specialCharsRule = /^[a-zA-Z0-9_\-.'*~\s]+$/;
const noSpaceRule = /^\S*$/;

const { specialCharsRule, noSpaceRule } = REGEX_RULES;
const validationSchema = Yup.object().shape({
displayName: Yup.string().required(
intl.formatMessage(messages.requiredFieldError),
Expand Down
2 changes: 1 addition & 1 deletion src/generic/data/apiHooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { getOrganizations, getTagsCount } from './api';
export const useOrganizationListData = () => (
useQuery({
queryKey: ['organizationList'],
queryFn: () => getOrganizations(),
queryFn: getOrganizations,
})
);

Expand Down
27 changes: 0 additions & 27 deletions src/library-authoring/CreateLibrary.tsx

This file was deleted.

1 change: 1 addition & 0 deletions src/library-authoring/LibraryAuthoringPage.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const libraryData: ContentLibrary = {
hasUnpublishedChanges: true,
hasUnpublishedDeletes: false,
license: '',
canEditLibrary: false,
};

const RootWrapper = () => (
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryAuthoringPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import NotFoundAlert from '../generic/NotFoundAlert';
import LibraryComponents from './LibraryComponents';
import LibraryCollections from './LibraryCollections';
import LibraryHome from './LibraryHome';
import { useContentLibrary } from './data/apiHook';
import { useContentLibrary } from './data/apiHooks';
import messages from './messages';

enum TabList {
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryComponents.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';

import { NoComponents, NoSearchResults } from './EmptyStates';
import { useLibraryComponentCount } from './data/apiHook';
import { useLibraryComponentCount } from './data/apiHooks';
import messages from './messages';

type LibraryComponentsProps = {
Expand Down
2 changes: 1 addition & 1 deletion src/library-authoring/LibraryHome.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import {
import { NoComponents, NoSearchResults } from './EmptyStates';
import LibraryCollections from './LibraryCollections';
import LibraryComponents from './LibraryComponents';
import { useLibraryComponentCount } from './data/apiHook';
import { useLibraryComponentCount } from './data/apiHooks';
import messages from './messages';

const Section = ({ title, children } : { title: string, children: React.ReactNode }) => (
Expand Down
131 changes: 131 additions & 0 deletions src/library-authoring/create-library/CreateLibrary.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import React from 'react';
import MockAdapter from 'axios-mock-adapter';
import { initializeMockApp } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event';

import initializeStore from '../../store';
import CreateLibrary from './CreateLibrary';
import { getContentLibraryV2CreateApiUrl } from './data/api';

let store;
const mockNavigate = jest.fn();
let axiosMock: MockAdapter;

jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
}));

jest.mock('../../generic/data/apiHooks', () => ({
...jest.requireActual('../../generic/data/apiHooks'),
useOrganizationListData: () => ({
data: ['org1', 'org2', 'org3', 'org4', 'org5'],
isLoading: false,
}),
}));

const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
},
});

const RootWrapper = () => (
<AppProvider store={store}>
<IntlProvider locale="en" messages={{}}>
<QueryClientProvider client={queryClient}>
<CreateLibrary />
</QueryClientProvider>
</IntlProvider>
</AppProvider>
);

describe('<CreateLibrary />', () => {
beforeEach(() => {
initializeMockApp({
authenticatedUser: {
userId: 3,
username: 'abc123',
administrator: true,
roles: [],
},
});
store = initializeStore();

axiosMock = new MockAdapter(getAuthenticatedHttpClient());
});

afterEach(() => {
jest.clearAllMocks();
axiosMock.restore();
queryClient.clear();
});

test('call api data with correct data', async () => {
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, {
id: 'library-id',
});

const { getByRole } = render(<RootWrapper />);

const titleInput = getByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

const orgInput = getByRole('combobox', { name: /organization/i });
userEvent.click(orgInput);
userEvent.type(orgInput, 'org1');
userEvent.tab();

const slugInput = getByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(
'{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}',
);
expect(mockNavigate).toHaveBeenCalledWith('/library/library-id');
});
});

test('show api error', async () => {
axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, {
field: 'Error message',
});
const { getByRole, getByTestId } = render(<RootWrapper />);

const titleInput = getByRole('textbox', { name: /library name/i });
userEvent.click(titleInput);
userEvent.type(titleInput, 'Test Library Name');

const orgInput = getByTestId('autosuggest-textbox-input');
userEvent.click(orgInput);
userEvent.type(orgInput, 'org1');
userEvent.tab();

const slugInput = getByRole('textbox', { name: /library id/i });
userEvent.click(slugInput);
userEvent.type(slugInput, 'test_library_slug');

fireEvent.click(getByRole('button', { name: /create/i }));
await waitFor(() => {
expect(axiosMock.history.post.length).toBe(1);
expect(axiosMock.history.post[0].data).toBe(
'{"description":"","title":"Test Library Name","org":"org1","slug":"test_library_slug"}',
);
expect(mockNavigate).not.toHaveBeenCalled();
expect(getByRole('alert')).toHaveTextContent('Request failed with status code 400');
expect(getByRole('alert')).toHaveTextContent('{"field":"Error message"}');
});
});
});
Loading

0 comments on commit e087001

Please sign in to comment.