refactor: convert a couple files to TS and improve typings/tests (#1181)

* refactor: convert files to ts and improve typings/tests

* fix: set return type to unkown for future fix
This commit is contained in:
Rômulo Penido
2024-08-02 13:26:10 -03:00
committed by GitHub
parent cba85ab96d
commit 680b5ff160
7 changed files with 132 additions and 64 deletions

View File

@@ -15,7 +15,7 @@ const TagsSidebarHeader = () => {
const {
data: contentTagsCount,
isSuccess: isContentTagsCountLoaded,
} = useContentTagsCount(contentId || '');
} = useContentTagsCount(contentId);
return (
<Stack

View File

@@ -142,7 +142,7 @@ const CardHeader = ({
{(isVertical || isSequential) && (
<CardStatus status={status} showDiscussionsEnabledBadge={showDiscussionsEnabledBadge} />
)}
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && contentTagCount > 0 && (
{ getConfig().ENABLE_TAGGING_TAXONOMY_PAGES === 'true' && !!contentTagCount && (
<TagCount count={contentTagCount} onClick={openManageTagsDrawer} />
)}
<Dropdown data-testid={`${namePrefix}-card-header__menu`} onClick={onClickMenuButton}>

View File

@@ -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);
});
});

View File

@@ -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<string[]>}
*/
export async function getOrganizations() {
export async function getOrganizations(): Promise<string[]> {
const { data } = await getAuthenticatedHttpClient().get(
getOrganizationsUrl(),
);
@@ -24,9 +28,8 @@ export async function getOrganizations() {
/**
* Get's course rerun data.
* @returns {Promise<Object>}
*/
export async function getCourseRerun(courseId) {
export async function getCourseRerun(courseId: string): Promise<unknown> {
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<Object>}
*/
export async function createOrRerunCourse(courseData) {
export async function createOrRerunCourse(courseData: Object): Promise<unknown> {
const { data } = await getAuthenticatedHttpClient().post(
getCreateOrRerunCourseUrl(),
convertObjectToSnakeCase(courseData, true),
@@ -48,9 +49,8 @@ export async function createOrRerunCourse(courseData) {
/**
* Retrieves user's clipboard.
* @returns {Promise<Object>} - A Promise that resolves clipboard data.
*/
export async function getClipboard() {
export async function getClipboard(): Promise<unknown> {
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<Object>} - A Promise that resolves clipboard data.
*/
export async function updateClipboard(usageKey) {
export async function updateClipboard(usageKey: string): Promise<unknown> {
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<Object>}
*/
export async function getTagsCount(contentPattern) {
if (contentPattern) {
const { data } = await getAuthenticatedHttpClient()
.get(getTagsCountApiUrl(contentPattern));
return data;
export async function getTagsCount(contentPattern?: string): Promise<Record<string, number>> {
if (!contentPattern) {
throw new Error('contentPattern is required');
}
return null;
const { data } = await getAuthenticatedHttpClient()
.get(getTagsCountApiUrl(contentPattern));
return data;
}

View File

@@ -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 });
});
});

View File

@@ -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 }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);
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);
});
});

View File

@@ -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,
});
};