Skip to content

Commit

Permalink
fix: remove datatable row status and update tests
Browse files Browse the repository at this point in the history
  • Loading branch information
muselesscreator committed Jun 30, 2022
1 parent 46e11c5 commit 464c190
Show file tree
Hide file tree
Showing 9 changed files with 686 additions and 92 deletions.
516 changes: 458 additions & 58 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@edx/brand": "npm:@edx/brand-edx.org@^1.3.2",
"@edx/frontend-component-footer": "10.2.1",
"@edx/frontend-component-header": "2.4.5",
"@edx/frontend-platform": "1.15.1",
"@edx/frontend-platform": "2.3.0",
"@edx/paragon": "19.6.0",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import { bulkManagementColumns } from 'data/constants/app';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';

jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));

jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,12 +43,6 @@ Array [

exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
<DataTable
EmptyTableComponent={[Function]}
FilterStatusComponent={[Function]}
RowStatusComponent={[Function]}
SelectionStatusComponent={[Function]}
additionalColumns={Array []}
bulkActions={Array []}
className="table-striped"
columns={
Array [
Expand Down Expand Up @@ -118,31 +112,7 @@ exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] =
},
]
}
dataViewToggleOptions={
Object {
"defaultActiveStateValue": "card",
"isDataViewToggleEnabled": false,
"onDataViewToggle": [Function],
"togglePlacement": "left",
}
}
defaultColumnValues={Object {}}
fetchData={null}
hasFixedColumnWidths={true}
initialState={Object {}}
initialTableOptions={Object {}}
isExpandable={false}
isFilterable={false}
isLoading={false}
isPaginated={false}
isSelectable={false}
isSortable={false}
itemCount={2}
manualFilters={false}
manualPagination={false}
manualSortBy={false}
numBreakoutFilters={1}
showFiltersInSidebar={false}
tableActions={Array []}
/>
`;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
className="gradebook-container"
>
<DataTable
RowStatusComponent={[MockFunction this.nullMethod]}
columns={
Array [
Object {
Expand Down Expand Up @@ -43,6 +44,18 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
hasFixedColumnWidths={true}
itemCount={3}
rowHeaderColumnKey="username"
/>
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable
content={
<FormattedMessage
defaultMessage="No results found"
description="Gradebook table message when no learner results were found"
id="gradebook.GradesView.table.noResultsFound"
/>
}
/>
</DataTable>
</div>
`;
12 changes: 11 additions & 1 deletion src/components/GradesView/GradebookTable/index.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export class GradebookTable extends React.Component {
super(props);
this.mapHeaders = this.mapHeaders.bind(this);
this.mapRows = this.mapRows.bind(this);
this.nullMethod = this.nullMethod.bind(this);
}

mapHeaders(heading) {
Expand Down Expand Up @@ -59,6 +60,10 @@ export class GradebookTable extends React.Component {
return dataRow;
}

nullMethod() {
return null;
}

render() {
return (
<div className="gradebook-container">
Expand All @@ -68,7 +73,12 @@ export class GradebookTable extends React.Component {
rowHeaderColumnKey="username"
hasFixedColumnWidths
itemCount={this.props.grades.length}
/>
RowStatusComponent={this.nullMethod}
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable content={<FormattedMessage {...messages.noResultsFound} />} />
</DataTable>
</div>
);
}
Expand Down
5 changes: 5 additions & 0 deletions src/components/GradesView/GradebookTable/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const messages = defineMessages({
defaultMessage: 'Total Grade values are always displayed as a percentage',
description: 'Gradebook table message that total grades are displayed in percent format',
},
noResultsFound: {
id: 'gradebook.GradesView.table.noResultsFound',
defaultMessage: 'No results found',
description: 'Gradebook table message when no learner results were found',
},
});

export default messages;
9 changes: 8 additions & 1 deletion src/components/GradesView/GradebookTable/test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,13 @@ import Fields from './Fields';
import messages from './messages';
import { GradebookTable, mapStateToProps } from '.';

jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
DataTable: {
Table: 'DataTable.Table',
TableControlBar: 'DataTable.TableControlBar',
EmptyTable: 'DataTable.EmptyTable',
},
}));
jest.mock('./Fields', () => ({
__esModule: true,
default: {
Expand Down Expand Up @@ -77,6 +83,7 @@ describe('GradebookTable', () => {
};
test('snapshot - fields1 and 2 between email and totalGrade, mocked rows', () => {
el = shallow(<GradebookTable {...props} />);
el.instance().nullMethod = jest.fn().mockName('this.nullMethod');
el.instance().mapRows = (entry) => `mappedRow: ${entry.percent}`;
expect(el.instance().render()).toMatchSnapshot();
});
Expand Down
187 changes: 187 additions & 0 deletions src/testUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
import react from 'react';

import { StrictDict } from 'utils';

/**
* Mocked formatMessage provided by react-intl
*/
export const formatMessage = (msg, values) => {
let message = msg.defaultMessage;
if (values === undefined) {
return message;
}
Object.keys(values).forEach((key) => {
// eslint-disable-next-line
message = message.replace(`{${key}}`, values[key]);
});
return message;
};

/**
* Mock a single component, or a nested component so that its children render nicely
* in snapshots.
* @param {string} name - parent component name
* @param {obj} contents - object of child components with intended component
* render name.
* @return {func} - mock component with nested children.
*
* usage:
* mockNestedComponent('Card', { Body: 'Card.Body', Form: { Control: { Feedback: 'Form.Control.Feedback' }}... });
* mockNestedComponent('IconButton', 'IconButton');
*/
export const mockNestedComponent = (name, contents) => {
if (typeof contents !== 'object') {
return contents;
}
const fn = () => name;
Object.defineProperty(fn, 'name', { value: name });
Object.keys(contents).forEach((nestedName) => {
const value = contents[nestedName];
fn[nestedName] = typeof value !== 'object'
? value
: mockNestedComponent(`${name}.${nestedName}`, value);
});
return fn;
};

/**
* Mock a module of components. nested components will be rendered nicely in snapshots.
* @param {obj} mapping - component module mock config.
* @return {obj} - module of flat and nested components that will render nicely in snapshots.
* usage:
* mockNestedComponents({
* Card: { Body: 'Card.Body' },
* IconButton: 'IconButton',
* })
*/
export const mockNestedComponents = (mapping) => Object.entries(mapping).reduce(
(obj, [name, value]) => ({
...obj,
[name]: mockNestedComponent(name, value),
}),
{},
);

/**
* Mock utility for working with useState in a hooks module.
* Expects/requires an object containing the state object in order to ensure
* the mock behavior works appropriately.
*
* Expected format:
* hooks = { state: { <key>: (val) => React.createRef(val), ... } }
*
* Returns a utility for mocking useState and providing access to specific state values
* and setState methods, as well as allowing per-test configuration of useState value returns.
*
* Example usage:
* // hooks.js
* import * as module from './hooks';
* const state = {
* isOpen: (val) => React.useState(val),
* hasDoors: (val) => React.useState(val),
* selected: (val) => React.useState(val),
* };
* ...
* export const exampleHook = () => {
* const [isOpen, setIsOpen] = module.state.isOpen(false);
* if (!isOpen) { return null; }
* return { isOpen, setIsOpen };
* }
* ...
*
* // hooks.test.js
* import * as hooks from './hooks';
* const state = new MockUseState(hooks)
* ...
* describe('state hooks', () => {
* state.testGetter(state.keys.isOpen);
* state.testGetter(state.keys.hasDoors);
* state.testGetter(state.keys.selected);
* });
* describe('exampleHook', () => {
* beforeEach(() => { state.mock(); });
* it('returns null if isOpen is default value', () => {
* expect(hooks.exampleHook()).toEqual(null);
* });
* it('returns isOpen and setIsOpen if isOpen is not null', () => {
* state.mockVal(state.keys.isOpen, true);
* expect(hooks.exampleHook()).toEqual({
* isOpen: true,
* setIsOpen: state.setState[state.keys.isOpen],
* });
* });
* afterEach(() => { state.restore(); });
* });
*
* @param {obj} hooks - hooks module containing a 'state' object
*/
export class MockUseState {
constructor(hooks) {
this.hooks = hooks;
this.oldState = null;
this.setState = {};
this.stateVals = {};

this.mock = this.mock.bind(this);
this.restore = this.restore.bind(this);
this.mockVal = this.mockVal.bind(this);
this.testGetter = this.testGetter.bind(this);
}

/**
* @return {object} - StrictDict of state object keys
*/
get keys() {
return StrictDict(Object.keys(this.hooks.state).reduce(
(obj, key) => ({ ...obj, [key]: key }),
{},
));
}

/**
* Replace the hook module's state object with a mocked version, initialized to default values.
*/
mock() {
this.oldState = this.hooks.state;
Object.keys(this.keys).forEach(key => {
this.hooks.state[key] = jest.fn(val => {
this.stateVals[key] = val;
return [val, this.setState[key]];
});
});
this.setState = Object.keys(this.keys).reduce(
(obj, key) => ({
...obj,
[key]: jest.fn(val => {
this.hooks.state[key] = val;
}),
}),
{},
);
}

/**
* Restore the hook module's state object to the actual code.
*/
restore() {
this.hooks.state = this.oldState;
}

/**
* Mock the state getter associated with a single key to return a specific value one time.
* @param {string} key - state key (from this.keys)
* @param {any} val - new value to be returned by the useState call.
*/
mockVal(key, val) {
this.hooks.state[key].mockReturnValueOnce([val, this.setState[key]]);
}

testGetter(key) {
test(`${key} state getter should return useState passthrough`, () => {
const testValue = 'some value';
const useState = (val) => ({ useState: val });
jest.spyOn(react, 'useState').mockImplementationOnce(useState);
expect(this.hooks.state[key](testValue)).toEqual(useState(testValue));
});
}
}

0 comments on commit 464c190

Please sign in to comment.