Skip to content

Commit

Permalink
refactor: convert a couple files to TS and improve typings/tests (#1181)
Browse files Browse the repository at this point in the history
* refactor: convert files to ts and improve typings/tests

* fix: set return type to unkown for future fix
  • Loading branch information
rpenido authored Aug 2, 2024
1 parent cba85ab commit 680b5ff
Show file tree
Hide file tree
Showing 7 changed files with 132 additions and 64 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const TagsSidebarHeader = () => {
const {
data: contentTagsCount,
isSuccess: isContentTagsCountLoaded,
} = useContentTagsCount(contentId || '');
} = useContentTagsCount(contentId);

return (
<Stack
Expand Down
2 changes: 1 addition & 1 deletion src/course-outline/card-header/CardHeader.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}>
Expand Down
7 changes: 4 additions & 3 deletions src/generic/data/api.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});
45 changes: 21 additions & 24 deletions src/generic/data/api.js → src/generic/data/api.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
// @ts-check
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';

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(),
);
Expand All @@ -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),
);
Expand All @@ -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),
Expand All @@ -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());

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

Expand All @@ -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;
}
28 changes: 0 additions & 28 deletions src/generic/data/apiHooks.test.js

This file was deleted.

99 changes: 99 additions & 0 deletions src/generic/data/apiHooks.test.tsx
Original file line number Diff line number Diff line change
@@ -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);
});
});
13 changes: 6 additions & 7 deletions src/generic/data/apiHooks.js → src/generic/data/apiHooks.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
// @ts-check
import { useQuery } from '@tanstack/react-query';
import { getOrganizations, getTagsCount } from './api';

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

0 comments on commit 680b5ff

Please sign in to comment.