diff --git a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx index ea154574b0..9101d30f2f 100644 --- a/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx +++ b/src/content-tags-drawer/tags-sidebar-controls/TagsSidebarHeader.jsx @@ -15,7 +15,7 @@ const TagsSidebarHeader = () => { const { data: contentTagsCount, isSuccess: isContentTagsCountLoaded, - } = useContentTagsCount(contentId || ''); + } = useContentTagsCount(contentId); return ( )} - { getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && contentTagCount > 0 && ( + { getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && ( )} diff --git a/src/generic/data/api.test.js b/src/generic/data/api.test.js index 96b6634d54..eeac9c65cd 100644 --- a/src/generic/data/api.test.js +++ b/src/generic/data/api.test.js @@ -86,8 +86,9 @@ describe('generic api calls', () => { expect(contentTagsCountMock[contentId]).toEqual(15); }); - it('should get null on empty pattern', async () => { - const result = await getTagsCount(''); - expect(result).toEqual(null); + it('should throw an error if no pattern is provided', async () => { + const pattern = undefined; + expect(getTagsCount(pattern)).rejects.toThrow('contentPattern is required'); + expect(axiosMock.history.get.length).toEqual(0); }); }); diff --git a/src/generic/data/api.js b/src/generic/data/api.ts similarity index 58% rename from src/generic/data/api.js rename to src/generic/data/api.ts index 83fd561ff3..fbec8b55bf 100644 --- a/src/generic/data/api.js +++ b/src/generic/data/api.ts @@ -1,4 +1,3 @@ -// @ts-check import { camelCaseObject, getConfig } from '@edx/frontend-platform'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; @@ -6,16 +5,21 @@ import { convertObjectToSnakeCase } from '../../utils'; export const getApiBaseUrl = () => getConfig().STUDIO_BASE_URL; export const getCreateOrRerunCourseUrl = () => new URL('course/', getApiBaseUrl()).href; -export const getCourseRerunUrl = (courseId) => new URL(`/api/contentstore/v1/course_rerun/${courseId}`, getApiBaseUrl()).href; +export const getCourseRerunUrl = (courseId: string) => new URL( + `/api/contentstore/v1/course_rerun/${courseId}`, + getApiBaseUrl(), +).href; export const getOrganizationsUrl = () => new URL('organizations', getApiBaseUrl()).href; export const getClipboardUrl = () => `${getApiBaseUrl()}/api/content-staging/v1/clipboard/`; -export const getTagsCountApiUrl = (contentPattern) => new URL(`api/content_tagging/v1/object_tag_counts/${contentPattern}/?count_implicit`, getApiBaseUrl()).href; +export const getTagsCountApiUrl = (contentPattern: string) => new URL( + `api/content_tagging/v1/object_tag_counts/${contentPattern}/?count_implicit`, + getApiBaseUrl(), +).href; /** * Get's organizations data. Returns list of organization names. - * @returns {Promise} */ -export async function getOrganizations() { +export async function getOrganizations(): Promise { const { data } = await getAuthenticatedHttpClient().get( getOrganizationsUrl(), ); @@ -24,9 +28,8 @@ export async function getOrganizations() { /** * Get's course rerun data. - * @returns {Promise} */ -export async function getCourseRerun(courseId) { +export async function getCourseRerun(courseId: string): Promise { const { data } = await getAuthenticatedHttpClient().get( getCourseRerunUrl(courseId), ); @@ -35,10 +38,8 @@ export async function getCourseRerun(courseId) { /** * Create or rerun course with data. - * @param {object} courseData - * @returns {Promise} */ -export async function createOrRerunCourse(courseData) { +export async function createOrRerunCourse(courseData: Object): Promise { const { data } = await getAuthenticatedHttpClient().post( getCreateOrRerunCourseUrl(), convertObjectToSnakeCase(courseData, true), @@ -48,9 +49,8 @@ export async function createOrRerunCourse(courseData) { /** * Retrieves user's clipboard. - * @returns {Promise} - A Promise that resolves clipboard data. */ -export async function getClipboard() { +export async function getClipboard(): Promise { const { data } = await getAuthenticatedHttpClient() .get(getClipboardUrl()); @@ -59,10 +59,8 @@ export async function getClipboard() { /** * Updates user's clipboard. - * @param {string} usageKey - The ID of the block. - * @returns {Promise} - A Promise that resolves clipboard data. */ -export async function updateClipboard(usageKey) { +export async function updateClipboard(usageKey: string): Promise { const { data } = await getAuthenticatedHttpClient() .post(getClipboardUrl(), { usage_key: usageKey }); @@ -71,15 +69,14 @@ export async function updateClipboard(usageKey) { /** * Gets the tags count of multiple content by id separated by commas or a pattern using a '*' wildcard. - * @param {string} contentPattern - * @returns {Promise} */ -export async function getTagsCount(contentPattern) { - if (contentPattern) { - const { data } = await getAuthenticatedHttpClient() - .get(getTagsCountApiUrl(contentPattern)); - - return data; +export async function getTagsCount(contentPattern?: string): Promise> { + if (!contentPattern) { + throw new Error('contentPattern is required'); } - return null; + + const { data } = await getAuthenticatedHttpClient() + .get(getTagsCountApiUrl(contentPattern)); + + return data; } diff --git a/src/generic/data/apiHooks.test.js b/src/generic/data/apiHooks.test.js deleted file mode 100644 index feb6f7fd59..0000000000 --- a/src/generic/data/apiHooks.test.js +++ /dev/null @@ -1,28 +0,0 @@ -import { useQuery } from '@tanstack/react-query'; -import { useContentTagsCount } from './apiHooks'; - -jest.mock('@tanstack/react-query', () => ({ - useQuery: jest.fn(), -})); - -jest.mock('./api', () => ({ - getTagsCount: jest.fn(), -})); - -describe('useContentTagsCount', () => { - it('should return success response', () => { - useQuery.mockReturnValueOnce({ isSuccess: true, data: 'data' }); - const pattern = '123'; - const result = useContentTagsCount(pattern); - - expect(result).toEqual({ isSuccess: true, data: 'data' }); - }); - - it('should return failure response', () => { - useQuery.mockReturnValueOnce({ isSuccess: false }); - const pattern = '123'; - const result = useContentTagsCount(pattern); - - expect(result).toEqual({ isSuccess: false }); - }); -}); diff --git a/src/generic/data/apiHooks.test.tsx b/src/generic/data/apiHooks.test.tsx new file mode 100644 index 0000000000..84168305a4 --- /dev/null +++ b/src/generic/data/apiHooks.test.tsx @@ -0,0 +1,99 @@ +import React from 'react'; +import { initializeMockApp } from '@edx/frontend-platform'; +import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; +import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { renderHook } from '@testing-library/react-hooks'; +import MockAdapter from 'axios-mock-adapter'; + +import { getTagsCountApiUrl } from './api'; +import { useContentTagsCount } from './apiHooks'; + +let axiosMock; + +const queryClient = new QueryClient({ + defaultOptions: { + queries: { + retry: false, + }, + }, +}); + +const wrapper = ({ children }) => ( + + {children} + +); + +describe('useContentTagsCount', () => { + beforeEach(() => { + initializeMockApp({ + authenticatedUser: { + userId: 3, + username: 'abc123', + administrator: true, + roles: [], + }, + }); + axiosMock = new MockAdapter(getAuthenticatedHttpClient()); + }); + + afterEach(() => { + jest.clearAllMocks(); + axiosMock.restore(); + }); + + it('should return success response', async () => { + const courseId = 'course-v1:edX+TestX+Test_Course'; + axiosMock.onGet(getTagsCountApiUrl(courseId)).reply(200, { [courseId]: 10 }); + + const hook = renderHook(() => useContentTagsCount(courseId), { wrapper }); + await hook.waitForNextUpdate(); + const { data, isSuccess } = hook.result.current; + + expect(axiosMock.history.get[0].url).toEqual(getTagsCountApiUrl(courseId)); + expect(isSuccess).toEqual(true); + expect(data).toEqual(10); + }); + + it('should return failure response', async () => { + const courseId = 'course-v1:edX+TestX+Test_Course'; + axiosMock.onGet(getTagsCountApiUrl(courseId)).reply(500, 'error'); + + const hook = renderHook(() => useContentTagsCount(courseId), { wrapper }); + await hook.waitForNextUpdate(); + + const { isSuccess } = hook.result.current; + + expect(axiosMock.history.get[0].url).toEqual(getTagsCountApiUrl(courseId)); + expect(isSuccess).toEqual(false); + }); + + it('should use an wildcard if a block is provided', async () => { + const blockId = 'block-v1:edX+TestX+Test_Course+type@chapter+block@123'; + const pattern = 'block-v1:edX+TestX+Test_Course*'; + axiosMock.onGet(getTagsCountApiUrl(pattern)).reply(200, { + [blockId]: 10, + 'block-v1:edX+TestX+Test_Course+type@chapter+block@another_block': 5, + }); + + const hook = renderHook(() => useContentTagsCount(blockId), { wrapper }); + await hook.waitForNextUpdate(); + + const { data, isSuccess } = hook.result.current; + + expect(axiosMock.history.get[0].url).toEqual(getTagsCountApiUrl(pattern)); + expect(isSuccess).toEqual(true); + expect(data).toEqual(10); + }); + + it('shouldnt call api if no pattern is provided', () => { + const hook = renderHook(() => useContentTagsCount(undefined), { wrapper }); + + hook.rerender(); + + const { isSuccess } = hook.result.current; + + expect(axiosMock.history.get.length).toEqual(0); + expect(isSuccess).toEqual(false); + }); +}); diff --git a/src/generic/data/apiHooks.js b/src/generic/data/apiHooks.ts similarity index 70% rename from src/generic/data/apiHooks.js rename to src/generic/data/apiHooks.ts index 7fec094dd6..38b1b16f83 100644 --- a/src/generic/data/apiHooks.js +++ b/src/generic/data/apiHooks.ts @@ -1,4 +1,3 @@ -// @ts-check import { useQuery } from '@tanstack/react-query'; import { getOrganizations, getTagsCount } from './api'; @@ -15,11 +14,10 @@ export const useOrganizationListData = () => ( /** * Builds the query to get tags count of the whole contentId course and * returns the tags count of the specific contentId. - * @param {string} contentId */ -export const useContentTagsCount = (contentId) => { - let contentPattern; - if (contentId.includes('course-v1')) { +export const useContentTagsCount = (contentId?: string) => { + let contentPattern: string | undefined; + if (!contentId || contentId.includes('course-v1')) { // If the contentId is a course, we want to get the tags count only for the course contentPattern = contentId; } else { @@ -28,7 +26,8 @@ export const useContentTagsCount = (contentId) => { } return useQuery({ queryKey: ['contentTagsCount', contentPattern], - queryFn: /* istanbul ignore next */ () => getTagsCount(contentPattern), - select: (data) => data[contentId] || 0, // Return the tags count of the specific contentId + queryFn: () => getTagsCount(contentPattern), + select: (data) => (contentId ? (data[contentId] || 0) : 0), // Return the tags count of the specific contentId + enabled: !!contentId, }); };