Paragon table deprecation updation (#246)
* refactor: updated table deprecation of HistoryTable * refactor: updated table deprecation of OverrideTable * refactor: updated table deprecation of GradebookTable * refactor: updated unit tests of OverrideTable according to new changes * fix: error for css * fix: strict dictionary error for react component * chore: update css and handle syntax errors * chore: update unit test * fix: remove datatable row status and update tests * fix: test coverage Co-authored-by: Leangseu Kim <lkim@edx.org> Co-authored-by: Ben Warzeski <bwarzeski@edx.org>
This commit is contained in:
committed by
GitHub
parent
bf70fd1450
commit
bbb7e895a5
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
import selectors from 'data/selectors';
|
||||
@@ -30,14 +30,13 @@ export const mapHistoryRows = ({
|
||||
export const HistoryTable = ({
|
||||
bulkManagementHistory,
|
||||
}) => (
|
||||
<>
|
||||
<Table
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
/>
|
||||
</>
|
||||
<DataTable
|
||||
data={bulkManagementHistory.map(mapHistoryRows)}
|
||||
hasFixedColumnWidths
|
||||
columns={bulkManagementColumns}
|
||||
className="table-striped"
|
||||
itemCount={bulkManagementHistory.length}
|
||||
/>
|
||||
);
|
||||
HistoryTable.defaultProps = {
|
||||
bulkManagementHistory: [],
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
/* eslint-disable import/no-named-as-default */
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
import { bulkManagementColumns } from 'data/constants/app';
|
||||
@@ -9,13 +9,12 @@ 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',
|
||||
}));
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
}));
|
||||
jest.mock('data/selectors', () => ({
|
||||
__esModule: true,
|
||||
default: {
|
||||
@@ -62,7 +61,7 @@ describe('HistoryTable', () => {
|
||||
describe('history table', () => {
|
||||
let table;
|
||||
beforeEach(() => {
|
||||
table = el.find(Table);
|
||||
table = el.find(DataTable);
|
||||
});
|
||||
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
|
||||
const fieldAssertions = [
|
||||
|
||||
@@ -42,78 +42,77 @@ Array [
|
||||
`;
|
||||
|
||||
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
|
||||
<Fragment>
|
||||
<Table
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "filename",
|
||||
"label": "Gradebook",
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "resultsSummary",
|
||||
"label": "Download Summary",
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "user",
|
||||
"label": "Who",
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"columnSortable": false,
|
||||
"key": "timeUploaded",
|
||||
"label": "When",
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
/>
|
||||
</Fragment>
|
||||
<DataTable
|
||||
className="table-striped"
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": "Gradebook",
|
||||
"accessor": "filename",
|
||||
"columnSortable": false,
|
||||
"width": "col-5",
|
||||
},
|
||||
Object {
|
||||
"Header": "Download Summary",
|
||||
"accessor": "resultsSummary",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
Object {
|
||||
"Header": "Who",
|
||||
"accessor": "user",
|
||||
"columnSortable": false,
|
||||
"width": "col-1",
|
||||
},
|
||||
Object {
|
||||
"Header": "When",
|
||||
"accessor": "timeUploaded",
|
||||
"columnSortable": false,
|
||||
"width": "col",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
blue.png
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="Da Bu Dee"
|
||||
rowId={12}
|
||||
text="Da ba daa"
|
||||
/>,
|
||||
"timeUploaded": "65",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Eifel
|
||||
</span>,
|
||||
},
|
||||
Object {
|
||||
"filename": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
allStar.jpg
|
||||
</span>,
|
||||
"resultsSummary": <ResultsSummary
|
||||
courseId="rockstar"
|
||||
rowId={2}
|
||||
text="all that glitters is gold"
|
||||
/>,
|
||||
"timeUploaded": "2000s?",
|
||||
"user": <span
|
||||
className="wrap-text-in-cell"
|
||||
>
|
||||
Smashmouth
|
||||
</span>,
|
||||
},
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -1,40 +1,40 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`OverrideTable Component snapshots basic snapshot shows a row for each entry and one editable row 1`] = `
|
||||
<Table
|
||||
<DataTable
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "date",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Date"
|
||||
description="Edit Modal Override Table Date column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.dateHeader"
|
||||
/>,
|
||||
"accessor": "date",
|
||||
},
|
||||
Object {
|
||||
"key": "grader",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Grader"
|
||||
description="Edit Modal Override Table Grader column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.graderHeader"
|
||||
/>,
|
||||
"accessor": "grader",
|
||||
},
|
||||
Object {
|
||||
"key": "reason",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Reason"
|
||||
description="Edit Modal Override Table Reason column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.reasonHeader"
|
||||
/>,
|
||||
"accessor": "reason",
|
||||
},
|
||||
Object {
|
||||
"key": "adjustedGrade",
|
||||
"label": <FormattedMessage
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Adjusted grade"
|
||||
description="Edit Modal Override Table Adjusted grade column header"
|
||||
id="gradebook.GradesView.EditModal.Overrides.adjustedGradeHeader"
|
||||
/>,
|
||||
"accessor": "adjustedGrade",
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -59,5 +59,6 @@ exports[`OverrideTable Component snapshots basic snapshot shows a row for each e
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
/>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
|
||||
@@ -27,14 +27,14 @@ export const OverrideTable = ({
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Table
|
||||
<DataTable
|
||||
columns={[
|
||||
{ label: <FormattedMessage {...messages.dateHeader} />, key: columns.date },
|
||||
{ label: <FormattedMessage {...messages.graderHeader} />, key: columns.grader },
|
||||
{ label: <FormattedMessage {...messages.reasonHeader} />, key: columns.reason },
|
||||
{ Header: <FormattedMessage {...messages.dateHeader} />, accessor: columns.date },
|
||||
{ Header: <FormattedMessage {...messages.graderHeader} />, accessor: columns.grader },
|
||||
{ Header: <FormattedMessage {...messages.reasonHeader} />, accessor: columns.reason },
|
||||
{
|
||||
label: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
key: columns.adjustedGrade,
|
||||
Header: <FormattedMessage {...messages.adjustedGradeHeader} />,
|
||||
accessor: columns.adjustedGrade,
|
||||
},
|
||||
]}
|
||||
data={[
|
||||
@@ -45,6 +45,7 @@ export const OverrideTable = ({
|
||||
reason: <ReasonInput />,
|
||||
},
|
||||
]}
|
||||
itemCount={gradeOverrides.length}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
mapStateToProps,
|
||||
} from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({ Table: () => 'Table' }));
|
||||
jest.mock('@edx/paragon', () => ({ DataTable: () => 'DataTable' }));
|
||||
jest.mock('./ReasonInput', () => 'ReasonInput');
|
||||
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
|
||||
|
||||
|
||||
@@ -4,48 +4,58 @@ exports[`GradebookTable component snapshot - fields1 and 2 between email and tot
|
||||
<div
|
||||
className="gradebook-container"
|
||||
>
|
||||
<div
|
||||
className="gbook"
|
||||
<DataTable
|
||||
RowStatusComponent={[MockFunction this.nullMethod]}
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"Header": <UsernameLabelReplacement />,
|
||||
"accessor": "Username",
|
||||
},
|
||||
Object {
|
||||
"Header": <FormattedMessage
|
||||
defaultMessage="Email"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
"accessor": "Email",
|
||||
},
|
||||
Object {
|
||||
"Header": "field1",
|
||||
"accessor": "field1",
|
||||
},
|
||||
Object {
|
||||
"Header": "field2",
|
||||
"accessor": "field2",
|
||||
},
|
||||
Object {
|
||||
"Header": <TotalGradeLabelReplacement />,
|
||||
"accessor": "Total Grade (%)",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
itemCount={3}
|
||||
rowHeaderColumnKey="username"
|
||||
>
|
||||
<Table
|
||||
columns={
|
||||
Array [
|
||||
Object {
|
||||
"key": "Username",
|
||||
"label": <UsernameLabelReplacement />,
|
||||
},
|
||||
Object {
|
||||
"key": "Email",
|
||||
"label": <FormattedMessage
|
||||
defaultMessage="Email"
|
||||
description="Gradebook table email column header"
|
||||
id="gradebook.GradesView.table.headings.email"
|
||||
/>,
|
||||
},
|
||||
Object {
|
||||
"key": "field1",
|
||||
"label": "field1",
|
||||
},
|
||||
Object {
|
||||
"key": "field2",
|
||||
"label": "field2",
|
||||
},
|
||||
Object {
|
||||
"key": "Total Grade (%)",
|
||||
"label": <TotalGradeLabelReplacement />,
|
||||
},
|
||||
]
|
||||
<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"
|
||||
/>
|
||||
}
|
||||
data={
|
||||
Array [
|
||||
"mappedRow: 1",
|
||||
"mappedRow: 2",
|
||||
"mappedRow: 3",
|
||||
]
|
||||
}
|
||||
hasFixedColumnWidths={true}
|
||||
rowHeaderColumnKey="username"
|
||||
/>
|
||||
</div>
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
@@ -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) {
|
||||
@@ -40,7 +41,7 @@ export class GradebookTable extends React.Component {
|
||||
} else {
|
||||
label = heading;
|
||||
}
|
||||
return { label, key: heading };
|
||||
return { Header: label, accessor: heading };
|
||||
}
|
||||
|
||||
mapRows(entry) {
|
||||
@@ -59,17 +60,25 @@ export class GradebookTable extends React.Component {
|
||||
return dataRow;
|
||||
}
|
||||
|
||||
nullMethod() {
|
||||
return null;
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="gradebook-container">
|
||||
<div className="gbook">
|
||||
<Table
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
/>
|
||||
</div>
|
||||
<DataTable
|
||||
columns={this.props.headings.map(this.mapHeaders)}
|
||||
data={this.props.grades.map(this.mapRows)}
|
||||
rowHeaderColumnKey="username"
|
||||
hasFixedColumnWidths
|
||||
itemCount={this.props.grades.length}
|
||||
RowStatusComponent={this.nullMethod}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.EmptyTable content={<FormattedMessage {...messages.noResultsFound} />} />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Table } from '@edx/paragon';
|
||||
import { DataTable } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import selectors from 'data/selectors';
|
||||
@@ -11,8 +11,12 @@ import Fields from './Fields';
|
||||
import messages from './messages';
|
||||
import { GradebookTable, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('@edx/paragon', () => ({
|
||||
Table: () => 'Table',
|
||||
jest.mock('@edx/paragon', () => jest.requireActual('testUtils').mockNestedComponents({
|
||||
DataTable: {
|
||||
Table: 'DataTable.Table',
|
||||
TableControlBar: 'DataTable.TableControlBar',
|
||||
EmptyTable: 'DataTable.EmptyTable',
|
||||
},
|
||||
}));
|
||||
jest.mock('./Fields', () => ({
|
||||
__esModule: true,
|
||||
@@ -79,40 +83,45 @@ 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();
|
||||
});
|
||||
test('null method returns null for stub component', () => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
expect(el.instance().nullMethod()).toEqual(null);
|
||||
});
|
||||
describe('table columns (mapHeaders)', () => {
|
||||
let headings;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
headings = el.find(Table).props().columns;
|
||||
headings = el.find(DataTable).props().columns;
|
||||
});
|
||||
test('username sets key and replaces label with component', () => {
|
||||
test('username sets key and replaces Header with component', () => {
|
||||
const heading = headings[0];
|
||||
expect(heading.key).toEqual(Headings.username);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
expect(heading.accessor).toEqual(Headings.username);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.UsernameLabelReplacement);
|
||||
});
|
||||
test('email sets key and label from header', () => {
|
||||
test('email sets key and Header from header', () => {
|
||||
const heading = headings[1];
|
||||
expect(heading.key).toEqual(Headings.email);
|
||||
expect(heading.label).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
expect(heading.accessor).toEqual(Headings.email);
|
||||
expect(heading.Header).toEqual(<FormattedMessage {...messages.emailHeading} />);
|
||||
});
|
||||
test('subsections set key and label from header', () => {
|
||||
expect(headings[2]).toEqual({ key: fields.field1, label: fields.field1 });
|
||||
expect(headings[3]).toEqual({ key: fields.field2, label: fields.field2 });
|
||||
test('subsections set key and Header from header', () => {
|
||||
expect(headings[2]).toEqual({ accessor: fields.field1, Header: fields.field1 });
|
||||
expect(headings[3]).toEqual({ accessor: fields.field2, Header: fields.field2 });
|
||||
});
|
||||
test('totalGrade sets key and replaces label with component', () => {
|
||||
test('totalGrade sets key and replaces Header with component', () => {
|
||||
const heading = headings[4];
|
||||
expect(heading.key).toEqual(Headings.totalGrade);
|
||||
expect(heading.label.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
expect(heading.accessor).toEqual(Headings.totalGrade);
|
||||
expect(heading.Header.type).toEqual(LabelReplacements.TotalGradeLabelReplacement);
|
||||
});
|
||||
});
|
||||
describe('table data (mapRows)', () => {
|
||||
let rows;
|
||||
beforeEach(() => {
|
||||
el = shallow(<GradebookTable {...props} />);
|
||||
rows = el.find(Table).props().data;
|
||||
rows = el.find(DataTable).props().data;
|
||||
});
|
||||
describe.each([0, 1, 2])('gradeEntry($percent)', (gradeIndex) => {
|
||||
let row;
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
.import-grades-btn {
|
||||
margin-left: 20px;
|
||||
}
|
||||
.intervention-report-description: {
|
||||
.intervention-report-description {
|
||||
margin-right: 40px;
|
||||
}
|
||||
h4.step-message-1 {
|
||||
@@ -71,100 +71,6 @@
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.gbook {
|
||||
width: 100%;
|
||||
|
||||
.grade-button {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.student-key {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
#courseGradeTooltipIcon {
|
||||
float: right;
|
||||
}
|
||||
|
||||
.table thead tr {
|
||||
min-height: 60px;
|
||||
&:nth-child(1) {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 10;
|
||||
background-color: white;
|
||||
th {
|
||||
background-color: white;
|
||||
border-bottom: 1px solid $gray_200;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
thead, tbody, tr, td, th {
|
||||
display: block;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.table tr th:first-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
.table tr th:first-child,
|
||||
.table tr td:first-child {
|
||||
position: sticky;
|
||||
left: 0;
|
||||
z-index: 1; // to float over the following children in the side-scrolling case
|
||||
background: white;
|
||||
}
|
||||
|
||||
.table tr {
|
||||
th:nth-child(1),
|
||||
td:nth-child(1),
|
||||
th:nth-child(2),
|
||||
td:nth-child(2) {
|
||||
width: 240px;
|
||||
}
|
||||
th:nth-last-of-type(1) {
|
||||
width: 150px;
|
||||
}
|
||||
th, td {
|
||||
width: 120px;
|
||||
}
|
||||
}
|
||||
.table tbody th {
|
||||
font-weight: normal;
|
||||
}
|
||||
|
||||
.table {
|
||||
overflow-x: hidden;
|
||||
|
||||
height: 100%;
|
||||
|
||||
tbody {
|
||||
overflow-y: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
thead, tbody tr {
|
||||
display: table;
|
||||
table-layout: fixed;
|
||||
}
|
||||
|
||||
th {
|
||||
vertical-align: top;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
|
||||
.link-style {
|
||||
color: #0075b4;
|
||||
&:hover, &:focus {
|
||||
color: #004368;
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
.form-group, .pgn__form-group {
|
||||
label {
|
||||
font-weight: bold;
|
||||
|
||||
@@ -32,26 +32,26 @@ export const localFilterKeys = StrictDict({
|
||||
*/
|
||||
export const bulkManagementColumns = [
|
||||
{
|
||||
key: 'filename',
|
||||
label: 'Gradebook',
|
||||
accessor: 'filename',
|
||||
Header: 'Gradebook',
|
||||
columnSortable: false,
|
||||
width: 'col-5',
|
||||
},
|
||||
{
|
||||
key: 'resultsSummary',
|
||||
label: 'Download Summary',
|
||||
accessor: 'resultsSummary',
|
||||
Header: 'Download Summary',
|
||||
columnSortable: false,
|
||||
width: 'col',
|
||||
},
|
||||
{
|
||||
key: 'user',
|
||||
label: 'Who',
|
||||
accessor: 'user',
|
||||
Header: 'Who',
|
||||
columnSortable: false,
|
||||
width: 'col-1',
|
||||
},
|
||||
{
|
||||
key: 'timeUploaded',
|
||||
label: 'When',
|
||||
accessor: 'timeUploaded',
|
||||
Header: 'When',
|
||||
columnSortable: false,
|
||||
width: 'col',
|
||||
},
|
||||
|
||||
187
src/testUtils.js
Normal file
187
src/testUtils.js
Normal file
@@ -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));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,10 @@ const strictGet = (target, name) => {
|
||||
return target;
|
||||
}
|
||||
|
||||
if (name === '$$typeof') {
|
||||
return typeof target;
|
||||
}
|
||||
|
||||
if (name in target || name === '_reactFragment') {
|
||||
return target[name];
|
||||
}
|
||||
|
||||
@@ -45,6 +45,9 @@ describe('StrictDict', () => {
|
||||
it('allows entry listing', () => {
|
||||
expect(Object.entries(dict)).toEqual(Object.entries(rawDict));
|
||||
});
|
||||
it('allows $$typeof access', () => {
|
||||
expect(dict.$$typeof).toEqual(typeof rawDict);
|
||||
});
|
||||
describe('missing key', () => {
|
||||
it('logs error with target, name, and error stack', () => {
|
||||
// eslint-ignore-next-line no-unused-vars
|
||||
|
||||
Reference in New Issue
Block a user