Skip to content

Commit

Permalink
chore: Partial coverage on Courseware Search Hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
rijuma committed Oct 18, 2023
1 parent 0e6ef21 commit eeeaaaf
Show file tree
Hide file tree
Showing 4 changed files with 195 additions and 43 deletions.
59 changes: 30 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
"@fortawesome/react-fontawesome": "^0.1.4",
"@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1",
"@testing-library/react-hooks": "^8.0.1",
"classnames": "2.3.2",
"core-js": "3.22.2",
"history": "5.3.0",
Expand Down
35 changes: 21 additions & 14 deletions src/course-home/courseware-search/hooks.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import { useState, useLayoutEffect, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { debounce } from 'lodash';
import { fetchCoursewareSearchSettings } from '../data/thunks';

const DEBOUNCE_WAIT = 300; // ms

export function useCoursewareSearchFeatureFlag() {
const { courseId } = useParams();
const [enabled, setEnabled] = useState(false);
Expand All @@ -16,43 +19,47 @@ export function useCoursewareSearchFeatureFlag() {

export function useCoursewareSearchState() {
const enabled = useCoursewareSearchFeatureFlag();

if (!enabled) { return { show: false }; }

const show = useSelector(state => state.courseHome.showSearch);

return { show: enabled && show };
return { show };
}

export function useElementBoundingBox(elementId) {
const [elementInfo, setElementInfo] = useState(undefined);
const [info, setInfo] = useState(undefined);

const element = document.getElementById(elementId);

if (!element) {
console.warn(`useElementBoundingBox(): Unable to find element with id='${elementId}' in the document.'`); // eslint-disable-line no-console
console.warn(`useElementBoundingBox(): Unable to find element with id='${elementId}' in the document.`); // eslint-disable-line no-console
return undefined;
}

useLayoutEffect(() => {
useEffect(() => {
// Handler to call on window resize and scroll
function recalculate() {
const info = element.getBoundingClientRect();
setElementInfo(info);
const bounds = element.getBoundingClientRect();
setInfo(bounds);
}
const debouncedRecalculate = debounce(recalculate, DEBOUNCE_WAIT, { leading: true });

// Add event listener
global.addEventListener('resize', recalculate);
global.addEventListener('scroll', recalculate);
global.addEventListener('resize', debouncedRecalculate);
global.addEventListener('scroll', debouncedRecalculate);

// Call handler right away so state gets updated with initial window size
recalculate();
debouncedRecalculate();

// Remove event listener on cleanup
return () => {
global.removeEventListener('resize', recalculate);
global.removeEventListener('scroll', recalculate);
global.removeEventListener('resize', debouncedRecalculate);
global.removeEventListener('scroll', debouncedRecalculate);
};
}, []); // Empty array ensures that effect is only run on mount
}, []);

return elementInfo;
return info;
}

export function useLockScroll() {
Expand Down
143 changes: 143 additions & 0 deletions src/course-home/courseware-search/hooks.test.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
import { renderHook, act } from '@testing-library/react-hooks';
import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux';
import { fetchCoursewareSearchSettings } from '../data/thunks';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchState, useElementBoundingBox } from './hooks';

jest.mock('react-redux');
jest.mock('react-router-dom');
jest.mock('../data/thunks');

describe('CoursewareSearch Hooks', () => {
const courses = {
123: { enabled: true },
456: { enabled: false },
};

beforeEach(() => {
fetchCoursewareSearchSettings.mockImplementation((courseId) => Promise.resolve(courses[courseId]));
});

afterEach(() => {
jest.resetAllMocks();
});

describe('useCoursewareSearchFeatureFlag', () => {
const renderTestHook = async (enabled = true) => {
useParams.mockImplementation(() => ({ courseId: enabled ? 123 : 456 }));
let state;
await act(async () => { (state = renderHook(() => useCoursewareSearchFeatureFlag())); });
return state;
};

test('should return true if feature is enabled', async () => {
const state = await renderTestHook();
await state.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
expect(state.result.current).toBe(true);
});

test('should return false if feature is disabled', async () => {
const state = await renderTestHook(false);
await state.waitFor(() => expect(fetchCoursewareSearchSettings).toBeCalledTimes(1));
expect(state.result.current).toBe(false);
});
});

describe('useCoursewareSearchState', () => {
const renderTestHook = async ({ enabled, showSearch }) => {
useParams.mockImplementation(() => ({ courseId: enabled ? 123 : 456 }));
const mockedStoreState = { courseHome: { showSearch } };
useSelector.mockImplementation(selector => selector(mockedStoreState));

let state;
await act(async () => { (state = renderHook(() => useCoursewareSearchState())); });
return state;
};

test('should return show: true if feature is enabled and showSearch is true', async () => {
const state = await renderTestHook({ enabled: true, showSearch: true });

expect(state.result.current).toEqual({ show: true });
});

test('should return show: false in any other case', async () => {
let state;

state = await renderTestHook({ enabled: true, showSearch: false });
expect(state.result.current).toEqual({ show: false });

state = await renderTestHook({ enabled: false, showSearch: true });
expect(state.result.current).toEqual({ show: false });

state = await renderTestHook({ enabled: false, showSearch: false });
expect(state.result.current).toEqual({ show: false });
});
});

describe('useElementBoundingBox', () => {
let getBoundingClientRectSpy;
let addEventListenerSpy;
let removeEventListenerSpy;

const renderTestHook = async ({ elementId, mockedInfo }) => {
getBoundingClientRectSpy = jest.spyOn(document, 'getElementById').mockImplementation(() => (
mockedInfo
? { getBoundingClientRect: () => ({ ...mockedInfo }) }
: undefined
));

let hook;
await act(async () => {
hook = renderHook(() => useElementBoundingBox(elementId));
});

return hook;
};

beforeEach(() => {
addEventListenerSpy = jest.spyOn(global, 'addEventListener');
removeEventListenerSpy = jest.spyOn(global, 'removeEventListener');
});

describe('when element is present', () => {
const mockedInfo = { top: 128 };

test('should bind resize and scroll events on mount', async () => {
await renderTestHook({ elementId: 'test', mockedInfo });

expect(addEventListenerSpy).toHaveBeenCalledWith('resize', expect.anything());
expect(addEventListenerSpy).toHaveBeenCalledWith('scroll', expect.anything());
});

test('should unbindbind resize and scroll events when unmounted', async () => {
const state = await renderTestHook({ elementId: 'test', mockedInfo });
state.unmount();

expect(removeEventListenerSpy).toHaveBeenCalledWith('resize', expect.anything());
expect(removeEventListenerSpy).toHaveBeenCalledWith('scroll', expect.anything());
});

// This test is failing, the hook state is not being updated.
xtest('should return the element bounding box', async () => {
const state = await renderTestHook({ elementId: 'test', mockedInfo });

state.waitFor(() => expect(getBoundingClientRectSpy).toHaveBeenCalled());

expect(state.result.current).toEqual(mockedInfo);
});

// This test is failing, the hook state is not being updated.
xtest('should call getBoundingClientRect on window resize', async () => {
const state = await renderTestHook({ elementId: 'test', mockedInfo });

act(() => {
// Trigger the window resize event.
global.innerWidth = 500;
global.dispatchEvent(new Event('resize'));
});

expect(state.result.current).toEqual(mockedInfo);
});
});
});
});

0 comments on commit eeeaaaf

Please sign in to comment.