From d8bc6c1fdcbd40ab633971df4c26b0612f2c5fa9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Tue, 21 Nov 2023 17:47:46 -0300 Subject: [PATCH 1/4] feat: import tags to existing taxonomy --- src/taxonomy/import-tags/__mocks__/index.js | 2 +- .../import-tags/__mocks__/tagImportMock.js | 4 + src/taxonomy/import-tags/data/api.js | 31 ++++++- src/taxonomy/import-tags/data/api.test.js | 18 +++- src/taxonomy/import-tags/data/utils.js | 93 +++++++++++++------ src/taxonomy/import-tags/index.js | 5 +- src/taxonomy/import-tags/messages.js | 7 ++ .../taxonomy-card/TaxonomyCardMenu.jsx | 8 +- src/taxonomy/taxonomy-card/index.jsx | 4 +- src/taxonomy/taxonomy-card/messages.js | 4 + 10 files changed, 138 insertions(+), 38 deletions(-) create mode 100644 src/taxonomy/import-tags/__mocks__/tagImportMock.js diff --git a/src/taxonomy/import-tags/__mocks__/index.js b/src/taxonomy/import-tags/__mocks__/index.js index 84c5b352ac..ba0b48ccb9 100644 --- a/src/taxonomy/import-tags/__mocks__/index.js +++ b/src/taxonomy/import-tags/__mocks__/index.js @@ -1,2 +1,2 @@ -// eslint-disable-next-line import/prefer-default-export export { default as taxonomyImportMock } from './taxonomyImportMock'; +export { default as tagImportMock } from './tagImportMock'; diff --git a/src/taxonomy/import-tags/__mocks__/tagImportMock.js b/src/taxonomy/import-tags/__mocks__/tagImportMock.js new file mode 100644 index 0000000000..9db45b4a5e --- /dev/null +++ b/src/taxonomy/import-tags/__mocks__/tagImportMock.js @@ -0,0 +1,4 @@ +export default { + name: 'Taxonomy name', + description: 'Taxonomy description', +}; diff --git a/src/taxonomy/import-tags/data/api.js b/src/taxonomy/import-tags/data/api.js index 53cb1cb0f3..befb2e977d 100644 --- a/src/taxonomy/import-tags/data/api.js +++ b/src/taxonomy/import-tags/data/api.js @@ -4,11 +4,20 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; -export const getTaxonomyImportApiUrl = () => new URL( +export const getTaxonomyImportNewApiUrl = () => new URL( 'api/content_tagging/v1/taxonomies/import/', getApiBaseUrl(), ).href; +/** + * @param {number} taxonomyId + * @returns {string} + */ +export const getTagsImportApiUrl = (taxonomyId) => new URL( + `api/content_tagging/v1/taxonomies/${taxonomyId}/tags/import/`, + getApiBaseUrl(), +).href; + /** * Import a new taxonomy * @param {string} taxonomyName @@ -23,7 +32,25 @@ export async function importNewTaxonomy(taxonomyName, taxonomyDescription, file) formData.append('file', file); const { data } = await getAuthenticatedHttpClient().post( - getTaxonomyImportApiUrl(), + getTaxonomyImportNewApiUrl(), + formData, + ); + + return camelCaseObject(data); +} + +/** + * Import tags to an existing taxonomy, overwriting existing tags + * @param {number} taxonomyId + * @param {File} file + * @returns {Promise} + */ +export async function importTags(taxonomyId, file) { + const formData = new FormData(); + formData.append('file', file); + + const { data } = await getAuthenticatedHttpClient().put( + getTagsImportApiUrl(taxonomyId), formData, ); diff --git a/src/taxonomy/import-tags/data/api.test.js b/src/taxonomy/import-tags/data/api.test.js index c40053cc53..e2022a2df6 100644 --- a/src/taxonomy/import-tags/data/api.test.js +++ b/src/taxonomy/import-tags/data/api.test.js @@ -2,11 +2,13 @@ import MockAdapter from 'axios-mock-adapter'; import { initializeMockApp } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; -import { taxonomyImportMock } from '../__mocks__'; +import { tagImportMock, taxonomyImportMock } from '../__mocks__'; import { - getTaxonomyImportApiUrl, + getTaxonomyImportNewApiUrl, + getTagsImportApiUrl, importNewTaxonomy, + importTags, } from './api'; let axiosMock; @@ -28,11 +30,19 @@ describe('import taxonomy api calls', () => { jest.clearAllMocks(); }); - it('should call import taxonomy', async () => { - axiosMock.onPost(getTaxonomyImportApiUrl()).reply(201, taxonomyImportMock); + it('should call import new taxonomy', async () => { + axiosMock.onPost(getTaxonomyImportNewApiUrl()).reply(201, taxonomyImportMock); const result = await importNewTaxonomy('Taxonomy name', 'Taxonomy description'); expect(axiosMock.history.post[0].url).toEqual(getTaxonomyImportApiUrl()); expect(result).toEqual(taxonomyImportMock); }); + + it('should call import tags', async () => { + axiosMock.onPost(getTagsImportApiUrl(1)).reply(200, tagImportMock); + const result = await importTags(1); + + expect(axiosMock.history.post[0].url).toEqual(getTagsImportApiUrl(1)); + expect(result).toEqual(tagImportMock); + }); }); diff --git a/src/taxonomy/import-tags/data/utils.js b/src/taxonomy/import-tags/data/utils.js index 027ff5065b..38e17fa48a 100644 --- a/src/taxonomy/import-tags/data/utils.js +++ b/src/taxonomy/import-tags/data/utils.js @@ -1,7 +1,41 @@ +// ts-check import messages from '../messages'; -import { importNewTaxonomy } from './api'; +import { importNewTaxonomy, importTags } from './api'; + +/* + * This function get a file from the user. It does this by creating a + * file input element, and then clicking it. This allows us to get a file + * from the user without using a form. The file input element is created + * and appended to the DOM, then clicked. When the user selects a file, + * the change event is fired, and the file is resolved. + * The file input element is then removed from the DOM. +*/ +const selectFile = async () => new Promise((resolve) => { + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.json,.csv'; + fileInput.style.display = 'none'; + fileInput.addEventListener('change', (event) => { + const file = event.target.files[0]; + if (!file) { + resolve(null); + } + resolve(file); + document.body.removeChild(fileInput); + }, false); + + fileInput.addEventListener('cancel', () => { + resolve(null); + document.body.removeChild(fileInput); + }, false); + + document.body.appendChild(fileInput); + + // Calling click() directly was not working as expected, so we use setTimeout + // to ensure the file input is added to the DOM before clicking it. + setTimeout(() => fileInput.click(), 0); +}); -// eslint-disable-next-line import/prefer-default-export export const importTaxonomy = async (intl) => { /* * This function is a temporary "Barebones" implementation of the import @@ -12,31 +46,6 @@ export const importTaxonomy = async (intl) => { /* eslint-disable no-alert */ /* eslint-disable no-console */ - const selectFile = async () => new Promise((resolve) => { - /* - * This function get a file from the user. It does this by creating a - * file input element, and then clicking it. This allows us to get a file - * from the user without using a form. The file input element is created - * and appended to the DOM, then clicked. When the user selects a file, - * the change event is fired, and the file is resolved. - * The file input element is then removed from the DOM. - */ - const fileInput = document.createElement('input'); - fileInput.type = 'file'; - fileInput.accept = '.json,.csv'; - fileInput.addEventListener('change', (event) => { - const file = event.target.files[0]; - if (!file) { - resolve(null); - } - resolve(file); - document.body.removeChild(fileInput); - }); - - document.body.appendChild(fileInput); - fileInput.click(); - }); - const getTaxonomyName = () => { let taxonomyName = null; while (!taxonomyName) { @@ -80,3 +89,33 @@ export const importTaxonomy = async (intl) => { console.error(error.response); }); }; + +export const importTaxonomyTags = async (taxonomyId, intl) => { + /* + * This function is a temporary "Barebones" implementation of the import + * functionality with `confirm` and `alert`. It is intended to be replaced + * with a component that shows a `ModalDialog` in the future. + * See: https://github.com/openedx/modular-learning/issues/126 + */ + /* eslint-disable no-alert */ + /* eslint-disable no-console */ + console.log(intl); + const file = await selectFile(); + + if (!file) { + return; + } + + if (!window.confirm(intl.formatMessage(messages.confirmImportTags))) { + return; + } + + importTags(taxonomyId, file) + .then(() => { + alert(intl.formatMessage(messages.importTaxonomySuccess)); + }) + .catch((error) => { + alert(intl.formatMessage(messages.importTaxonomyError)); + console.error(error.response); + }); +}; diff --git a/src/taxonomy/import-tags/index.js b/src/taxonomy/import-tags/index.js index bfd7f80725..40100badeb 100644 --- a/src/taxonomy/import-tags/index.js +++ b/src/taxonomy/import-tags/index.js @@ -1,5 +1,6 @@ -import { importTaxonomy } from './data/utils'; +import { importTaxonomyTags, importTaxonomy } from './data/utils'; export { - importTaxonomy, // eslint-disable-line import/prefer-default-export + importTaxonomyTags, + importTaxonomy, }; diff --git a/src/taxonomy/import-tags/messages.js b/src/taxonomy/import-tags/messages.js index f9a1ff273b..eaa6780d9f 100644 --- a/src/taxonomy/import-tags/messages.js +++ b/src/taxonomy/import-tags/messages.js @@ -1,3 +1,4 @@ +// ts-check import { defineMessages } from '@edx/frontend-platform/i18n'; const messages = defineMessages({ @@ -21,6 +22,12 @@ const messages = defineMessages({ id: 'course-authoring.import-tags.error', defaultMessage: 'Import failed - see details in the browser console', }, + confirmImportTags: { + id: 'course-authoring.import-tags.warning', + defaultMessage: 'Warning! You are about to overwrite all tags in this taxonomy. Any tags applied to course' + + ' content will be updated or removed. This cannot be undone.' + + '\n\nAre you sure you want to continue importing this file?', + }, }); export default messages; diff --git a/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx b/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx index 1b27c8ee61..6fef8b672e 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx +++ b/src/taxonomy/taxonomy-card/TaxonomyCardMenu.jsx @@ -31,7 +31,13 @@ const TaxonomyCardMenu = ({ data-testid={`taxonomy-card-menu-button-${id}`} /> - {/* Add more menu items here */} + onClickItem(e, 'import')} + > + {intl.formatMessage(messages.taxonomyCardImportMenu)} + { const intl = useIntl(); const [isExportModalOpen, setIsExportModalOpen] = useState(false); - // Add here more menu item actions const menuItemActions = { + import: () => importTaxonomyTags(id, intl).then(() => console.log('resolved')), export: () => setIsExportModalOpen(true), }; diff --git a/src/taxonomy/taxonomy-card/messages.js b/src/taxonomy/taxonomy-card/messages.js index 6886c2f99c..def583a96d 100644 --- a/src/taxonomy/taxonomy-card/messages.js +++ b/src/taxonomy/taxonomy-card/messages.js @@ -17,6 +17,10 @@ const messages = defineMessages({ id: 'course-authoring.taxonomy-list.orgs-count.label', defaultMessage: 'Assigned to {orgsCount} orgs', }, + taxonomyCardImportMenu: { + id: 'course-authoring.taxonomy-list.menu.import.label', + defaultMessage: 'Re-import', + }, taxonomyCardExportMenu: { id: 'course-authoring.taxonomy-list.menu.export.label', defaultMessage: 'Export', From 1ac94e8f744e484a9c3ba654e12dd05da1ab4a33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 22 Nov 2023 12:34:41 -0300 Subject: [PATCH 2/4] feat: add re-import menu to taxonomy detail --- src/taxonomy/taxonomy-card/index.jsx | 2 +- .../taxonomy-detail/TaxonomyDetailMenu.jsx | 3 +++ .../taxonomy-detail/TaxonomyDetailPage.jsx | 16 ++++++++-------- src/taxonomy/taxonomy-detail/messages.js | 4 ++++ 4 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/taxonomy/taxonomy-card/index.jsx b/src/taxonomy/taxonomy-card/index.jsx index 5ea83ee151..09072ca1ab 100644 --- a/src/taxonomy/taxonomy-card/index.jsx +++ b/src/taxonomy/taxonomy-card/index.jsx @@ -75,7 +75,7 @@ const TaxonomyCard = ({ className, original }) => { const [isExportModalOpen, setIsExportModalOpen] = useState(false); const menuItemActions = { - import: () => importTaxonomyTags(id, intl).then(() => console.log('resolved')), + import: () => importTaxonomyTags(id, intl).then(), export: () => setIsExportModalOpen(true), }; diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx index 1f2aa839d2..38ce2492d9 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailMenu.jsx @@ -20,6 +20,9 @@ const TaxonomyDetailMenu = ({ alt={intl.formatMessage(messages.actionsButtonAlt, { name })} disabled={disabled} > + onClickMenuItem('import')}> + {intl.formatMessage(messages.importMenu)} + onClickMenuItem('export')}> {intl.formatMessage(messages.exportMenu)} diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx index a22fb0bd57..d2f0361ef0 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.jsx @@ -11,6 +11,7 @@ import { Link, useParams } from 'react-router-dom'; import ConnectionErrorAlert from '../../generic/ConnectionErrorAlert'; import Loading from '../../generic/Loading'; import SubHeader from '../../generic/sub-header/SubHeader'; +import { importTaxonomyTags } from '../import-tags'; import taxonomyMessages from '../messages'; import TaxonomyDetailMenu from './TaxonomyDetailMenu'; import TaxonomyDetailSideCard from './TaxonomyDetailSideCard'; @@ -46,16 +47,15 @@ const TaxonomyDetailPage = () => { /> ); - const onClickMenuItem = (menuName) => { - switch (menuName) { - case 'export': - setIsExportModalOpen(true); - break; - default: - break; - } + const menuItemActions = { + import: () => importTaxonomyTags(taxonomyId, intl).then(), + export: () => setIsExportModalOpen(true), }; + const onClickMenuItem = (menuName) => ( + menuItemActions[menuName]?.() + ); + const getHeaderActions = () => ( Date: Wed, 22 Nov 2023 12:34:54 -0300 Subject: [PATCH 3/4] test: add tests --- src/taxonomy/import-tags/data/api.test.js | 6 +- src/taxonomy/import-tags/data/utils.test.js | 342 ++++++++++++------ .../taxonomy-card/TaxonomyCard.test.jsx | 15 + .../TaxonomyDetailPage.test.jsx | 25 ++ 4 files changed, 272 insertions(+), 116 deletions(-) diff --git a/src/taxonomy/import-tags/data/api.test.js b/src/taxonomy/import-tags/data/api.test.js index e2022a2df6..0da9f84eae 100644 --- a/src/taxonomy/import-tags/data/api.test.js +++ b/src/taxonomy/import-tags/data/api.test.js @@ -34,15 +34,15 @@ describe('import taxonomy api calls', () => { axiosMock.onPost(getTaxonomyImportNewApiUrl()).reply(201, taxonomyImportMock); const result = await importNewTaxonomy('Taxonomy name', 'Taxonomy description'); - expect(axiosMock.history.post[0].url).toEqual(getTaxonomyImportApiUrl()); + expect(axiosMock.history.post[0].url).toEqual(getTaxonomyImportNewApiUrl()); expect(result).toEqual(taxonomyImportMock); }); it('should call import tags', async () => { - axiosMock.onPost(getTagsImportApiUrl(1)).reply(200, tagImportMock); + axiosMock.onPut(getTagsImportApiUrl(1)).reply(200, tagImportMock); const result = await importTags(1); - expect(axiosMock.history.post[0].url).toEqual(getTagsImportApiUrl(1)); + expect(axiosMock.history.put[0].url).toEqual(getTagsImportApiUrl(1)); expect(result).toEqual(tagImportMock); }); }); diff --git a/src/taxonomy/import-tags/data/utils.test.js b/src/taxonomy/import-tags/data/utils.test.js index 7e1c1c1ec4..ddcc029410 100644 --- a/src/taxonomy/import-tags/data/utils.test.js +++ b/src/taxonomy/import-tags/data/utils.test.js @@ -1,5 +1,5 @@ -import { importTaxonomy } from './utils'; -import { importNewTaxonomy } from './api'; +import { importTaxonomy, importTaxonomyTags } from './utils'; +import { importNewTaxonomy, importTags } from './api'; const mockAddEventListener = jest.fn(); @@ -9,9 +9,10 @@ const intl = { jest.mock('./api', () => ({ importNewTaxonomy: jest.fn().mockResolvedValue({}), + importTags: jest.fn().mockResolvedValue({}), })); -describe('import taxonomy functions', () => { +describe('import new taxonomy functions', () => { let createElement; let appendChild; let removeChild; @@ -23,6 +24,7 @@ describe('import taxonomy functions', () => { return { click: jest.fn(), addEventListener: mockAddEventListener, + style: {}, }; } return createElement(element); @@ -42,144 +44,258 @@ describe('import taxonomy functions', () => { document.body.removeChild = removeChild; }); - it('should call the api and show success alert', async () => { - jest.spyOn(window, 'prompt') - .mockReturnValueOnce('test taxonomy name') - .mockReturnValueOnce('test taxonomy description'); - jest.spyOn(window, 'alert').mockImplementation(() => {}); + describe('import new taxonomy', () => { + it('should call the api and show success alert', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); - expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + it('should ask for taxonomy name again if not provided', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('You must enter a name for the new taxonomy'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [ - 'mockFile', - ], - }, - }; + it('should call the api and return error alert', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce('test taxonomy description'); + importNewTaxonomy.mockRejectedValue(new Error('test error')); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); - onChange(mockTarget); + it('should abort the call to the api without file', async () => { + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [null], + }, + }; + + onChange(mockTarget); + return promise; + }); - return promise; - }); + it('should abort the call to the api if file closed', async () => { + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onCancel handler from the file input element + const onCancel = mockAddEventListener.mock.calls[1][1]; - it('should ask for taxonomy name again if not provided', async () => { - jest.spyOn(window, 'prompt') - .mockReturnValueOnce('') - .mockReturnValueOnce('test taxonomy name') - .mockReturnValueOnce('test taxonomy description'); - jest.spyOn(window, 'alert').mockImplementation(() => {}); - - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); - expect(window.alert).toHaveBeenCalledWith('You must enter a name for the new taxonomy'); - expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + onCancel(); + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [ - 'mockFile', - ], - }, - }; + it('should abort the call to the api when cancel name prompt', async () => { + jest.spyOn(window, 'prompt').mockReturnValueOnce(null); - onChange(mockTarget); + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); - return promise; - }); + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; - it('should call the api and return error alert', async () => { - jest.spyOn(window, 'prompt') - .mockReturnValueOnce('test taxonomy name') - .mockReturnValueOnce('test taxonomy description'); - importNewTaxonomy.mockRejectedValue(new Error('test error')); + onChange(mockTarget); - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).toHaveBeenCalledWith('test taxonomy name', 'test taxonomy description', 'mockFile'); + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [ - 'mockFile', - ], - }, - }; - - onChange(mockTarget); - - return promise; + it('should abort the call to the api when cancel description prompt', async () => { + jest.spyOn(window, 'prompt') + .mockReturnValueOnce('test taxonomy name') + .mockReturnValueOnce(null); + + const promise = importTaxonomy(intl).then(() => { + expect(importNewTaxonomy).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + return promise; + }); }); - it('should abort the call to the api without file', async () => { - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).not.toHaveBeenCalled(); + describe('import tags', () => { + it('should call the api and show success alert', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + jest.spyOn(window, 'alert').mockImplementation(() => {}); + + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).toHaveBeenCalledWith(1, 'mockFile'); + expect(window.alert).toHaveBeenCalledWith('Taxonomy imported successfully'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [null], - }, - }; + it('should abort the call to the api without file', async () => { + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [null], + }, + }; + + onChange(mockTarget); + return promise; + }); - onChange(mockTarget); - return promise; - }); + it('should abort the call to the api if file closed', async () => { + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); - it('should abort the call to the api when cancel name prompt', async () => { - jest.spyOn(window, 'prompt').mockReturnValueOnce(null); + // Capture the onCancel handler from the file input element + const onCancel = mockAddEventListener.mock.calls[1][1]; - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).not.toHaveBeenCalled(); + onCancel(); + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [ - 'mockFile', - ], - }, - }; + it('should abort the call to the api when cancel the confirm dialog', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(null); - onChange(mockTarget); + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).not.toHaveBeenCalled(); + }); - return promise; - }); + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; - it('should abort the call to the api when cancel description prompt', async () => { - jest.spyOn(window, 'prompt') - .mockReturnValueOnce('test taxonomy name') - .mockReturnValueOnce(null); + onChange(mockTarget); - const promise = importTaxonomy(intl).then(() => { - expect(importNewTaxonomy).not.toHaveBeenCalled(); + return promise; }); - // Capture the onChange handler from the file input element - const onChange = mockAddEventListener.mock.calls[0][1]; - const mockTarget = { - target: { - files: [ - 'mockFile', - ], - }, - }; - - onChange(mockTarget); - return promise; + it('should call the api and return error alert', async () => { + jest.spyOn(window, 'confirm').mockReturnValueOnce(true); + importTags.mockRejectedValue(new Error('test error')); + + const promise = importTaxonomyTags(1, intl).then(() => { + expect(importTags).toHaveBeenCalledWith(1, 'mockFile'); + }); + + // Capture the onChange handler from the file input element + const onChange = mockAddEventListener.mock.calls[0][1]; + const mockTarget = { + target: { + files: [ + 'mockFile', + ], + }, + }; + + onChange(mockTarget); + + return promise; + }); }); }); diff --git a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx index fd61bf0cdb..8033de6383 100644 --- a/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx +++ b/src/taxonomy/taxonomy-card/TaxonomyCard.test.jsx @@ -7,11 +7,16 @@ import PropTypes from 'prop-types'; import initializeStore from '../../store'; import { getTaxonomyExportFile } from '../data/api'; +import { importTaxonomyTags } from '../import-tags'; import TaxonomyCard from '.'; let store; const taxonomyId = 1; +jest.mock('../import-tags', () => ({ + importTaxonomyTags: jest.fn().mockResolvedValue({}), +})); + const data = { id: taxonomyId, name: 'Taxonomy 1', @@ -132,6 +137,16 @@ describe('', async () => { expect(() => getByText('Select format to export')).toThrow(); }); + test('should call import tags when menu click', () => { + const { getByTestId } = render(); + + // Click on import menu + fireEvent.click(getByTestId('taxonomy-card-menu-button-1')); + fireEvent.click(getByTestId('taxonomy-card-menu-import-1')); + + expect(importTaxonomyTags).toHaveBeenCalled(); + }); + test('should export a taxonomy', () => { const { getByTestId, getByText } = render(); diff --git a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx index 085ea59b85..23af888835 100644 --- a/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx +++ b/src/taxonomy/taxonomy-detail/TaxonomyDetailPage.test.jsx @@ -4,6 +4,7 @@ import { IntlProvider } from '@edx/frontend-platform/i18n'; import { AppProvider } from '@edx/frontend-platform/react'; import { fireEvent, render } from '@testing-library/react'; +import { importTaxonomyTags } from '../import-tags'; import { useTaxonomyDetailData } from './data/api'; import initializeStore from '../../store'; import TaxonomyDetailPage from './TaxonomyDetailPage'; @@ -13,6 +14,9 @@ let store; jest.mock('./data/api', () => ({ useTaxonomyDetailData: jest.fn(), })); +jest.mock('../import-tags', () => ({ + importTaxonomyTags: jest.fn().mockResolvedValue({}), +})); jest.mock('react-router-dom', () => ({ ...jest.requireActual('react-router-dom'), // use actual for all non-hook parts useParams: () => ({ @@ -108,4 +112,25 @@ describe('', async () => { // Modal closed expect(() => getByText('Select format to export')).toThrow(); }); + + it('should call import tags when menu clicked', () => { + useTaxonomyDetailData.mockReturnValue({ + isSuccess: true, + isFetched: true, + isError: false, + data: { + id: 1, + name: 'Test taxonomy', + description: 'This is a description', + }, + }); + + const { getByRole, getByText } = render(); + + // Click on import menu + fireEvent.click(getByRole('button')); + fireEvent.click(getByText('Re-import')); + + expect(importTaxonomyTags).toHaveBeenCalled(); + }); }); From 8c2de2372e8091696b01cb7d12a6e10f9d37d6d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C3=B4mulo=20Penido?= Date: Wed, 22 Nov 2023 22:07:56 -0300 Subject: [PATCH 4/4] fix: clean debug code Co-authored-by: Jillian --- src/taxonomy/import-tags/data/utils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/taxonomy/import-tags/data/utils.js b/src/taxonomy/import-tags/data/utils.js index 38e17fa48a..e46e76941e 100644 --- a/src/taxonomy/import-tags/data/utils.js +++ b/src/taxonomy/import-tags/data/utils.js @@ -99,7 +99,6 @@ export const importTaxonomyTags = async (taxonomyId, intl) => { */ /* eslint-disable no-alert */ /* eslint-disable no-console */ - console.log(intl); const file = await selectFile(); if (!file) {