From 35ee68ea9dd09b2b520cac76d2c85880e679d0fe Mon Sep 17 00:00:00 2001 From: Ben Warzeski Date: Thu, 11 May 2023 13:29:47 -0400 Subject: [PATCH] refactor: SearchControls component modernization --- .../__snapshots__/index.test.jsx.snap | 18 +++++ .../GradesView/SearchControls/hooks.js | 41 +++++++++++ .../GradesView/SearchControls/hooks.test.js | 71 +++++++++++++++++++ .../GradesView/SearchControls/index.jsx | 38 ++++++++++ .../GradesView/SearchControls/index.test.jsx | 48 +++++++++++++ .../GradesView/SearchControls/messages.js | 16 +++++ 6 files changed, 232 insertions(+) create mode 100644 src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap create mode 100644 src/components/GradesView/SearchControls/hooks.js create mode 100644 src/components/GradesView/SearchControls/hooks.test.js create mode 100644 src/components/GradesView/SearchControls/index.jsx create mode 100644 src/components/GradesView/SearchControls/index.test.jsx create mode 100644 src/components/GradesView/SearchControls/messages.js diff --git a/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap b/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap new file mode 100644 index 0000000..d9f5c94 --- /dev/null +++ b/src/components/GradesView/SearchControls/__snapshots__/index.test.jsx.snap @@ -0,0 +1,18 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`SearchControls component render snapshot 1`] = ` +
+ + + test-hint-text + +
+`; diff --git a/src/components/GradesView/SearchControls/hooks.js b/src/components/GradesView/SearchControls/hooks.js new file mode 100644 index 0000000..8ce4973 --- /dev/null +++ b/src/components/GradesView/SearchControls/hooks.js @@ -0,0 +1,41 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { actions, selectors, thunkActions } from 'data/redux/hooks'; + +import messages from './messages'; + +/** + * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer + * as well as the search box for searching by username/email. + */ +export const useSearchControlsData = () => { + const { formatMessage } = useIntl(); + const searchValue = selectors.app.useSearchValue(); + const fetchGrades = thunkActions.grades.useFetchGrades(); + const setSearchValue = actions.app.useSetSearchValue(); + + const onBlur = (e) => { + setSearchValue(e.target.value); + }; + + const onClear = () => { + setSearchValue(''); + fetchGrades(); + }; + + const onSubmit = (newValue) => { + setSearchValue(newValue); + fetchGrades(); + }; + + return { + onSubmit, + onBlur, + onClear, + searchValue, + inputLabel: formatMessage(messages.label), + hintText: formatMessage(messages.hint), + }; +}; + +export default useSearchControlsData; diff --git a/src/components/GradesView/SearchControls/hooks.test.js b/src/components/GradesView/SearchControls/hooks.test.js new file mode 100644 index 0000000..db0c291 --- /dev/null +++ b/src/components/GradesView/SearchControls/hooks.test.js @@ -0,0 +1,71 @@ +import { useIntl } from '@edx/frontend-platform/i18n'; + +import { formatMessage } from 'testUtils'; +import { actions, selectors, thunkActions } from 'data/redux/hooks'; + +import useSearchControlsData from './hooks'; +import messages from './messages'; + +jest.mock('data/redux/hooks', () => ({ + actions: { + app: { useSetSearchValue: jest.fn() }, + }, + selectors: { + app: { useSearchValue: jest.fn() }, + }, + thunkActions: { + grades: { useFetchGrades: jest.fn() }, + }, +})); + +const searchValue = 'test-search-value'; +selectors.app.useSearchValue.mockReturnValue(searchValue); +const setSearchValue = jest.fn(); +actions.app.useSetSearchValue.mockReturnValue(setSearchValue); +const fetchGrades = jest.fn(); +thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades); + +const testValue = 'test-value'; +let out; +describe('useSearchControlsData', () => { + beforeEach(() => { + jest.clearAllMocks(); + out = useSearchControlsData(); + }); + describe('behavior', () => { + it('initializes intl hook', () => { + expect(useIntl).toHaveBeenCalled(); + }); + it('initializes redux hooks', () => { + expect(actions.app.useSetSearchValue).toHaveBeenCalled(); + expect(selectors.app.useSearchValue).toHaveBeenCalled(); + expect(thunkActions.grades.useFetchGrades).toHaveBeenCalled(); + }); + }); + describe('output', () => { + test('onSubmit sets search value and fetches grades', () => { + out.onSubmit(testValue); + expect(setSearchValue).toHaveBeenCalledWith(testValue); + expect(fetchGrades).toHaveBeenCalled(); + }); + test('onBlur sets search value to event target', () => { + out.onBlur({ target: { value: testValue } }); + expect(setSearchValue).toHaveBeenCalledWith(testValue); + expect(fetchGrades).not.toHaveBeenCalled(); + }); + test('onClear clears search value and fetches grades', () => { + out.onClear(); + expect(setSearchValue).toHaveBeenCalledWith(''); + expect(fetchGrades).toHaveBeenCalled(); + }); + it('forwards searchValue from redux', () => { + expect(out.searchValue).toEqual(searchValue); + }); + test('input label message', () => { + expect(out.inputLabel).toEqual(formatMessage(messages.label)); + }); + test('hint text message', () => { + expect(out.hintText).toEqual(formatMessage(messages.hint)); + }); + }); +}); diff --git a/src/components/GradesView/SearchControls/index.jsx b/src/components/GradesView/SearchControls/index.jsx new file mode 100644 index 0000000..17f418b --- /dev/null +++ b/src/components/GradesView/SearchControls/index.jsx @@ -0,0 +1,38 @@ +import React from 'react'; + +import { SearchField } from '@edx/paragon'; +import useSearchControlsData from './hooks'; + +/** + * Controls for filtering the GradebookTable. Contains the "Edit Filters" button for opening the filter drawer + * as well as the search box for searching by username/email. + */ +export const SearchControls = () => { + const { + onSubmit, + onBlur, + onClear, + searchValue, + inputLabel, + hintText, + } = useSearchControlsData(); + + return ( +
+ + + {hintText} + +
+ ); +}; + +SearchControls.propTypes = {}; + +export default SearchControls; diff --git a/src/components/GradesView/SearchControls/index.test.jsx b/src/components/GradesView/SearchControls/index.test.jsx new file mode 100644 index 0000000..ae9a102 --- /dev/null +++ b/src/components/GradesView/SearchControls/index.test.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import { SearchField } from '@edx/paragon'; + +import useSearchControlsData from './hooks'; +import SearchControls from '.'; + +jest.mock('./hooks', () => jest.fn()); + +const hookProps = { + onSubmit: jest.fn().mockName('hooks.onSubmit'), + onBlur: jest.fn().mockName('hooks.onBlur'), + onClear: jest.fn().mockName('hooks.onClear'), + searchValue: 'test-search-value', + inputLabel: 'test-input-label', + hintText: 'test-hint-text', +}; +useSearchControlsData.mockReturnValue(hookProps); + +let el; +describe('SearchControls component', () => { + beforeEach(() => { + jest.clearAllMocks(); + el = shallow(); + }); + describe('behavior', () => { + it('initializes component hooks', () => { + expect(useSearchControlsData).toHaveBeenCalled(); + }); + }); + describe('render', () => { + test('snapshot', () => { + expect(el).toMatchSnapshot(); + }); + test('search field', () => { + const props = el.find(SearchField).props(); + expect(props.onSubmit).toEqual(hookProps.onSubmit); + expect(props.onBlur).toEqual(hookProps.onBlur); + expect(props.onClear).toEqual(hookProps.onClear); + expect(props.inputLabel).toEqual(hookProps.inputLabel); + expect(props.value).toEqual(hookProps.searchValue); + }); + test('hint text', () => { + expect(el.find('small').text()).toEqual(hookProps.hintText); + }); + }); +}); diff --git a/src/components/GradesView/SearchControls/messages.js b/src/components/GradesView/SearchControls/messages.js new file mode 100644 index 0000000..79b117f --- /dev/null +++ b/src/components/GradesView/SearchControls/messages.js @@ -0,0 +1,16 @@ +import { defineMessages } from '@edx/frontend-platform/i18n'; + +const messages = defineMessages({ + label: { + id: 'gradebook.GradesView.search.label', + defaultMessage: 'Search for a learner', + description: 'Text prompting a user to use this functionality to search for a learner', + }, + hint: { + id: 'gradebook.GradesView.search.hint', + defaultMessage: 'Search by username, email, or student key', + description: 'A hint explaining the ways a user can search', + }, +}); + +export default messages;