From c3d5c17a46dc7064b80090f3813868a0553f2bda Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 10 Jan 2025 17:31:29 +1030 Subject: [PATCH 1/5] fix: allow user provided value if can auto-create orgs --- .../create-library/CreateLibrary.test.tsx | 36 +++++++++++++++++++ .../create-library/CreateLibrary.tsx | 10 +++++- 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index cf323b1aca..a72b77a94c 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -8,6 +8,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { fireEvent, render, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; +import { studioHomeMock } from '../../studio-home/__mocks__'; +import { getStudioHomeApiUrl } from '../../studio-home/data/api'; +import { getApiWaffleFlagsUrl } from '../../data/api'; import initializeStore from '../../store'; import { CreateLibrary } from '.'; import { getContentLibraryV2CreateApiUrl } from './data/api'; @@ -60,6 +63,9 @@ describe('', () => { store = initializeStore(); axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock + .onGet(getApiWaffleFlagsUrl(undefined)) + .reply(200, {}); }); afterEach(() => { @@ -69,6 +75,7 @@ describe('', () => { }); test('call api data with correct data', async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { id: 'library-id', }); @@ -98,7 +105,36 @@ describe('', () => { }); }); + test('cannot create new org unless allowed', async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); + axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { + id: 'library-id', + }); + + const { getByRole, getByText } = render(); + + 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, 'NewOrg'); + 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(0); + }); + expect(getByText('Required field.')).toBeInTheDocument(); + }); + test('show api error', async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, { field: 'Error message', }); diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index 1731984ec5..1a2b4fb4fb 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -19,6 +19,7 @@ import FormikErrorFeedback from '../../generic/FormikErrorFeedback'; import AlertError from '../../generic/alert-error'; import { useOrganizationListData } from '../../generic/data/apiHooks'; import SubHeader from '../../generic/sub-header/SubHeader'; +import { useStudioHome } from '../../studio-home/hooks'; import { useCreateLibraryV2 } from './data/apiHooks'; import messages from './messages'; @@ -42,6 +43,8 @@ const CreateLibrary = () => { isLoading: isOrganizationListLoading, } = useOrganizationListData(); + const { studioHomeData: { allowToCreateNewOrg } } = useStudioHome(); + const handleOnClickCancel = () => { navigate('/libraries'); }; @@ -100,7 +103,12 @@ const CreateLibrary = () => { formikProps.setFieldValue('org', event.selectionId)} + onChange={(event) => formikProps.setFieldValue( + 'org', + allowToCreateNewOrg + ? (event.selectionId || event.userProvidedText) + : event.selectionId, + )} placeholder={intl.formatMessage(messages.orgPlaceholder)} > {organizationListData ? organizationListData.map((org) => ( From 0e21a4c2d40031fe13d32a765ab16cbd60852607 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 10 Jan 2025 17:57:29 +1030 Subject: [PATCH 2/5] test: auto-create org --- .../create-library/CreateLibrary.test.tsx | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index a72b77a94c..2a3d83ec15 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -133,6 +133,40 @@ describe('', () => { expect(getByText('Required field.')).toBeInTheDocument(); }); + test('can create new org if allowed', async () => { + axiosMock.onGet(getStudioHomeApiUrl()).reply(200, { + ...studioHomeMock, + allow_to_create_new_org: true, + }); + axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { + id: 'library-id', + }); + + const { getByRole } = render(); + + 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, 'NewOrg'); + 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":"NewOrg","slug":"test_library_slug"}', + ); + expect(mockNavigate).toHaveBeenCalledWith('/library/library-id'); + }); + }); + test('show api error', async () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, studioHomeMock); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, { From 0773c4fab60a00663b02cee19fe881b8711cd5d8 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Fri, 10 Jan 2025 18:19:37 +1030 Subject: [PATCH 3/5] test: simplify CreateLibrary tests --- .../create-library/CreateLibrary.test.tsx | 58 +++++-------------- 1 file changed, 14 insertions(+), 44 deletions(-) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index 2a3d83ec15..20af0663f4 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -1,21 +1,19 @@ 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 type MockAdapter from 'axios-mock-adapter'; import userEvent from '@testing-library/user-event'; +import { + fireEvent, + initializeMocks, + render, + waitFor, +} from '../../testUtils'; import { studioHomeMock } from '../../studio-home/__mocks__'; import { getStudioHomeApiUrl } from '../../studio-home/data/api'; import { getApiWaffleFlagsUrl } from '../../data/api'; -import initializeStore from '../../store'; import { CreateLibrary } from '.'; import { getContentLibraryV2CreateApiUrl } from './data/api'; -let store; const mockNavigate = jest.fn(); let axiosMock: MockAdapter; @@ -32,37 +30,9 @@ jest.mock('../../generic/data/apiHooks', () => ({ }), })); -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - retry: false, - }, - }, -}); - -const RootWrapper = () => ( - - - - - - - -); - describe('', () => { beforeEach(() => { - initializeMockApp({ - authenticatedUser: { - userId: 3, - username: 'abc123', - administrator: true, - roles: [], - }, - }); - store = initializeStore(); - - axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + axiosMock = initializeMocks().axiosMock; axiosMock .onGet(getApiWaffleFlagsUrl(undefined)) .reply(200, {}); @@ -71,7 +41,6 @@ describe('', () => { afterEach(() => { jest.clearAllMocks(); axiosMock.restore(); - queryClient.clear(); }); test('call api data with correct data', async () => { @@ -80,7 +49,7 @@ describe('', () => { id: 'library-id', }); - const { getByRole } = render(); + const { getByRole } = render(); const titleInput = getByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); @@ -111,7 +80,7 @@ describe('', () => { id: 'library-id', }); - const { getByRole, getByText } = render(); + const { getByRole, getByText } = render(); const titleInput = getByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); @@ -137,12 +106,13 @@ describe('', () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, { ...studioHomeMock, allow_to_create_new_org: true, + allowToCreateNewOrg: true, }); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { id: 'library-id', }); - const { getByRole } = render(); + const { getByRole } = render(); const titleInput = getByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); @@ -172,7 +142,7 @@ describe('', () => { axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, { field: 'Error message', }); - const { getByRole, getByTestId } = render(); + const { getByRole, getByTestId } = render(); const titleInput = getByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); @@ -200,7 +170,7 @@ describe('', () => { }); test('cancel creating library navigates to libraries page', async () => { - const { getByRole } = render(); + const { getByRole } = render(); fireEvent.click(getByRole('button', { name: /cancel/i })); await waitFor(() => { From a25a09e053ea0e0d153bfd28280072ebe6e5475f Mon Sep 17 00:00:00 2001 From: Navin Karkera Date: Mon, 13 Jan 2025 11:44:37 +0530 Subject: [PATCH 4/5] test: fix org input by using async api --- .../create-library/CreateLibrary.test.tsx | 62 ++++++++++--------- 1 file changed, 32 insertions(+), 30 deletions(-) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index 20af0663f4..482221d927 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -3,9 +3,11 @@ import type MockAdapter from 'axios-mock-adapter'; import userEvent from '@testing-library/user-event'; import { + act, fireEvent, initializeMocks, render, + screen, waitFor, } from '../../testUtils'; import { studioHomeMock } from '../../studio-home/__mocks__'; @@ -49,22 +51,22 @@ describe('', () => { id: 'library-id', }); - const { getByRole } = render(); + render(); - const titleInput = getByRole('textbox', { name: /library name/i }); + const titleInput = await screen.findByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); - const orgInput = getByRole('combobox', { name: /organization/i }); + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); userEvent.click(orgInput); userEvent.type(orgInput, 'org1'); - userEvent.tab(); + act(() => userEvent.tab()); - const slugInput = getByRole('textbox', { name: /library id/i }); + const slugInput = await screen.findByRole('textbox', { name: /library id/i }); userEvent.click(slugInput); userEvent.type(slugInput, 'test_library_slug'); - fireEvent.click(getByRole('button', { name: /create/i })); + fireEvent.click(await screen.findByRole('button', { name: /create/i })); await waitFor(() => { expect(axiosMock.history.post.length).toBe(1); expect(axiosMock.history.post[0].data).toBe( @@ -80,26 +82,26 @@ describe('', () => { id: 'library-id', }); - const { getByRole, getByText } = render(); + render(); - const titleInput = getByRole('textbox', { name: /library name/i }); + const titleInput = await screen.findByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); - const orgInput = getByRole('combobox', { name: /organization/i }); + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); userEvent.click(orgInput); userEvent.type(orgInput, 'NewOrg'); - userEvent.tab(); + act(() => userEvent.tab()); - const slugInput = getByRole('textbox', { name: /library id/i }); + const slugInput = await screen.findByRole('textbox', { name: /library id/i }); userEvent.click(slugInput); userEvent.type(slugInput, 'test_library_slug'); - fireEvent.click(getByRole('button', { name: /create/i })); + fireEvent.click(await screen.findByRole('button', { name: /create/i })); await waitFor(() => { expect(axiosMock.history.post.length).toBe(0); }); - expect(getByText('Required field.')).toBeInTheDocument(); + expect(await screen.findByText('Required field.')).toBeInTheDocument(); }); test('can create new org if allowed', async () => { @@ -112,22 +114,22 @@ describe('', () => { id: 'library-id', }); - const { getByRole } = render(); + render(); - const titleInput = getByRole('textbox', { name: /library name/i }); + const titleInput = await screen.findByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); - const orgInput = getByRole('combobox', { name: /organization/i }); + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); userEvent.click(orgInput); userEvent.type(orgInput, 'NewOrg'); - userEvent.tab(); + act(() => userEvent.tab()); - const slugInput = getByRole('textbox', { name: /library id/i }); + const slugInput = await screen.findByRole('textbox', { name: /library id/i }); userEvent.click(slugInput); userEvent.type(slugInput, 'test_library_slug'); - fireEvent.click(getByRole('button', { name: /create/i })); + fireEvent.click(await screen.findByRole('button', { name: /create/i })); await waitFor(() => { expect(axiosMock.history.post.length).toBe(1); expect(axiosMock.history.post[0].data).toBe( @@ -142,37 +144,37 @@ describe('', () => { axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(400, { field: 'Error message', }); - const { getByRole, getByTestId } = render(); + render(); - const titleInput = getByRole('textbox', { name: /library name/i }); + const titleInput = await screen.findByRole('textbox', { name: /library name/i }); userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); - const orgInput = getByTestId('autosuggest-textbox-input'); + const orgInput = await screen.findByTestId('autosuggest-textbox-input'); userEvent.click(orgInput); userEvent.type(orgInput, 'org1'); - userEvent.tab(); + act(() => userEvent.tab()); - const slugInput = getByRole('textbox', { name: /library id/i }); + const slugInput = await screen.findByRole('textbox', { name: /library id/i }); userEvent.click(slugInput); userEvent.type(slugInput, 'test_library_slug'); - fireEvent.click(getByRole('button', { name: /create/i })); - await waitFor(() => { + fireEvent.click(await screen.findByRole('button', { name: /create/i })); + await waitFor(async () => { 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"}'); + expect(await screen.findByRole('alert')).toHaveTextContent('Request failed with status code 400'); + expect(await screen.findByRole('alert')).toHaveTextContent('{"field":"Error message"}'); }); }); test('cancel creating library navigates to libraries page', async () => { - const { getByRole } = render(); + render(); - fireEvent.click(getByRole('button', { name: /cancel/i })); + fireEvent.click(await screen.findByRole('button', { name: /cancel/i })); await waitFor(() => { expect(mockNavigate).toHaveBeenCalledWith('/libraries'); }); From 552ffcc69a644797c6538ce89033e68203879bb6 Mon Sep 17 00:00:00 2001 From: Jillian Vogel Date: Tue, 14 Jan 2025 13:41:05 +1030 Subject: [PATCH 5/5] fix: restrict available orgs on library creation --- .../create-library/CreateLibrary.test.tsx | 12 +++++++++++- .../create-library/CreateLibrary.tsx | 19 +++++++++++++++---- src/studio-home/__mocks__/studioHomeMock.js | 1 + 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/library-authoring/create-library/CreateLibrary.test.tsx b/src/library-authoring/create-library/CreateLibrary.test.tsx index 482221d927..d129cfed16 100644 --- a/src/library-authoring/create-library/CreateLibrary.test.tsx +++ b/src/library-authoring/create-library/CreateLibrary.test.tsx @@ -88,6 +88,12 @@ describe('', () => { userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); + // We cannot create a new org, and so we're restricted to the allowed list + const orgOptions = screen.getByTestId('autosuggest-iconbutton'); + userEvent.click(orgOptions); + expect(screen.getByText('org1')).toBeInTheDocument(); + ['org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).not.toBeInTheDocument()); + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); userEvent.click(orgInput); userEvent.type(orgInput, 'NewOrg'); @@ -108,7 +114,6 @@ describe('', () => { axiosMock.onGet(getStudioHomeApiUrl()).reply(200, { ...studioHomeMock, allow_to_create_new_org: true, - allowToCreateNewOrg: true, }); axiosMock.onPost(getContentLibraryV2CreateApiUrl()).reply(200, { id: 'library-id', @@ -120,6 +125,11 @@ describe('', () => { userEvent.click(titleInput); userEvent.type(titleInput, 'Test Library Name'); + // We can create a new org, so we're also allowed to use any existing org + const orgOptions = screen.getByTestId('autosuggest-iconbutton'); + userEvent.click(orgOptions); + ['org1', 'org2', 'org3', 'org4', 'org5'].forEach((org) => expect(screen.queryByText(org)).toBeInTheDocument()); + const orgInput = await screen.findByRole('combobox', { name: /organization/i }); userEvent.click(orgInput); userEvent.type(orgInput, 'NewOrg'); diff --git a/src/library-authoring/create-library/CreateLibrary.tsx b/src/library-authoring/create-library/CreateLibrary.tsx index 1a2b4fb4fb..c4f3695c7e 100644 --- a/src/library-authoring/create-library/CreateLibrary.tsx +++ b/src/library-authoring/create-library/CreateLibrary.tsx @@ -39,11 +39,22 @@ const CreateLibrary = () => { } = useCreateLibraryV2(); const { - data: organizationListData, + data: allOrganizations, isLoading: isOrganizationListLoading, } = useOrganizationListData(); - const { studioHomeData: { allowToCreateNewOrg } } = useStudioHome(); + const { + studioHomeData: { + allowedOrganizationsForLibraries, + allowToCreateNewOrg, + }, + } = useStudioHome(); + + const organizations = ( + allowToCreateNewOrg + ? allOrganizations + : allowedOrganizationsForLibraries + ) || []; const handleOnClickCancel = () => { navigate('/libraries'); @@ -111,9 +122,9 @@ const CreateLibrary = () => { )} placeholder={intl.formatMessage(messages.orgPlaceholder)} > - {organizationListData ? organizationListData.map((org) => ( + {organizations.map((org) => ( {org} - )) : []} + ))} {intl.formatMessage(messages.orgHelp)} diff --git a/src/studio-home/__mocks__/studioHomeMock.js b/src/studio-home/__mocks__/studioHomeMock.js index dcd313c511..cc135f259a 100644 --- a/src/studio-home/__mocks__/studioHomeMock.js +++ b/src/studio-home/__mocks__/studioHomeMock.js @@ -76,4 +76,5 @@ module.exports = { platformName: 'Your Platform Name Here', userIsActive: true, allowToCreateNewOrg: false, + allowedOrganizationsForLibraries: ['org1'], };