Compare commits

...

17 Commits

Author SHA1 Message Date
mashal-m
3a60955566 refactor: install browserlist config package 2022-10-05 18:08:12 +05:00
mashal-m
37966e86df refactor: use shared browserslist config 2022-10-05 17:57:30 +05:00
Simon Chen
03fa143fc1 Merge pull request #266 from fennec-tech/rtl-use-backslash-to-write-fractions
RTL: use backslash to write fractions (grades)
2022-09-29 15:02:24 -04:00
Simon Chen
075846f869 Merge pull request #265 from fennec-tech/rtl-fix-percentage-direction
RTL: fix (%) symbol to follow text direction
2022-09-29 14:49:14 -04:00
Abderraouf Mehdi Bouhali
1208d27d92 fix(rtl): use backslash to write fractions (grades) 2022-09-29 11:34:30 +01:00
Abderraouf Mehdi Bouhali
e345716bd4 fix(rtl): force % symbol to follow text direction 2022-09-29 10:11:26 +01:00
Maman Khan
2121a63c83 Removing codecov and coveralls packages (#245)
* fix: removed coveralls and codecov packages with update in ci uploader

* fix: pinning npm to version 8.5.x

Co-authored-by: Abdullah Waheed <abdullah.waheed@arbisoft.com>
2022-09-28 10:47:57 -04:00
Matt Hughes
47cab71b3c fix: do not squish the table with word wrapping
we should handle word wrapping down at the level of specific cells
which we can opt-in to non-default word-wrapping
2022-07-12 10:54:00 -04:00
Leangseu Kim
2d8af2ec00 fix: update and resolve dependencies 2022-07-07 13:23:23 -04:00
Leangseu Kim
d55abbe91e fix: search blur state for every single charater input 2022-07-07 10:05:35 -04:00
Abdullah Waheed
a75f365bdd refactor: migrated StatusAlert to Alert in GradesBook component
refactor: migrated StatusAlert to Alert in StatusAlerts component

refactor: updated unit tests mocks to set snapshot issues
2022-06-30 21:35:01 -04:00
Muhammad Abdullah Waheed
bbb7e895a5 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>
2022-06-30 14:22:29 -04:00
Muhammad Abdullah Waheed
bf70fd1450 fix: added specific npm version to fix peerDependency issue (#247) 2022-06-16 15:06:30 +05:00
Jawayria
af2ece8290 Merge pull request #242 from openedx/jenkins/version-check-c6a4685
feat: Add package-lock file version check
2022-05-06 15:56:23 +05:00
edX requirements bot
620827d772 feat: Add package-lock file version check 2022-04-29 08:48:48 -04:00
edX requirements bot
c6a4685bf5 chore!: Dropped support for Node 12 (#239)
* chore!: Dropped support for Node 12

* fix: dropped Node 12

Co-authored-by: Jawayria <39649635+Jawayria@users.noreply.github.com>
2022-04-14 12:53:54 -04:00
Leangseu Kim
8dd2237f9c chore: update package lock after update to node 16 2022-04-12 10:36:36 -04:00
35 changed files with 21834 additions and 20585 deletions

View File

@@ -3,17 +3,18 @@ name: node_js CI
on:
push:
branches:
- master
- master
pull_request:
branches:
- "**"
- '**'
jobs:
test:
runs-on: ubuntu-20.04
strategy:
matrix:
node: [12, 14, 16]
node: [16]
npm: [8.5.x]
steps:
- name: Checkout
@@ -24,6 +25,9 @@ jobs:
with:
node-version: ${{ matrix.node }}
- name: Install npm 8.5.x
run: npm install -g npm@${{ matrix.npm }}
- name: Install dependencies
run: npm ci
@@ -56,4 +60,5 @@ jobs:
subject: CI workflow failed in ${{github.repository}}
to: masters-grades@edx.org
from: github-actions <github-actions@edx.org>
body: CI workflow in ${{github.repository}} failed! For details see "github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}"
body: CI workflow in ${{github.repository}} failed! For details see "github.com/${{
github.repository }}/actions/runs/${{ github.run_id }}"

View File

@@ -0,0 +1,13 @@
#check package-lock file version
name: Lockfile Version check
on:
push:
branches:
- master
pull_request:
jobs:
version-check:
uses: edx/.github/.github/workflows/lockfileversion-check.yml@master

View File

@@ -14,7 +14,7 @@ tx_url2 = https://www.transifex.com/api/2/project/edx-platform/resource/$(transi
# This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-react-intl
NPM_TESTS=build i18n_extract lint test is-es5
NPM_TESTS=build i18n_extract lint test
.PHONY: test
test: $(addprefix test.npm.,$(NPM_TESTS)) ## validate ci suite

View File

@@ -1,4 +1,4 @@
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook) [![Coveralls](https://img.shields.io/coveralls/edx/frontend-app-gradebook.svg?branch=master)](https://coveralls.io/github/edx/frontend-app-gradebook)
[![Build Status](https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master)](https://travis-ci.com/edx/frontend-app-gradebook)
[![npm_version](https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![npm_downloads](https://img.shields.io/npm/dt/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)
[![license](https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg)](@edx/frontend-app-gradebook)

View File

@@ -25,10 +25,6 @@ This is important because `npm` uses the pinned dependencies in your `package-lo
However, when these changes surface within a CI build, this indicates differing dependency expectations between the committed `package.json` file and the `package-lock.json` file, which is a good reason to fail a build.
### What is this `npm run is-es5` check?
This project outputs production files to the `dist` folder. The `npm script`, `npm run is-es5`, checks the JavaScript files in the `dist` folder to make sure that they are `ES5`-compliant.
This check is important because `ES5` JavaScript has [greater browser compatibility](http://kangax.github.io/compat-table/es5/) than [`ES2015+`](http://kangax.github.io/compat-table/es6/) - particularly for `IE11`.
### `deploy` step

View File

@@ -11,5 +11,6 @@ module.exports = createConfig('jest', {
coveragePathIgnorePatterns: [
'src/segment.js',
'src/postcss.config.js',
'testUtils', // don't unit test jest mocking tools
],
});

41504
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -8,8 +8,6 @@
},
"scripts": {
"build": "fedx-scripts webpack",
"coveralls": "cat ./coverage/lcov.info | coveralls",
"is-es5": "es-check es5 ./dist/*.js",
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
@@ -26,15 +24,14 @@
"access": "public"
},
"browserslist": [
"last 2 versions",
"not ie > 0",
"not ie_mob > 0"
"extends @edx/browserslist-config"
],
"dependencies": {
"@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/browserslist-config": "^1.1.1",
"@edx/frontend-component-footer": "^11.1.1",
"@edx/frontend-component-header": "^3.1.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",
@@ -72,9 +69,7 @@
"@edx/frontend-build": "9.1.1",
"axios": "0.21.1",
"axios-mock-adapter": "^1.17.0",
"codecov": "^3.6.1",
"enzyme-adapter-react-16": "^1.14.0",
"es-check": "^2.3.0",
"fetch-mock": "^6.5.2",
"husky": "2.7.0",
"identity-obj-proxy": "^3.0.0",

View File

@@ -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: [],

View File

@@ -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 = [

View File

@@ -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}
/>
`;

View File

@@ -7,6 +7,7 @@ import { Form } from '@edx/paragon';
import selectors from 'data/selectors';
import actions from 'data/actions';
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
/**
* <AdjustedGradeInput />
@@ -32,7 +33,7 @@ export class AdjustedGradeInput extends React.Component {
value={this.props.value}
onChange={this.onChange}
/>
{this.props.possibleGrade && ` / ${this.props.possibleGrade}`}
{this.props.possibleGrade && ` ${isRtl(getLocale()) ? '\\' : '/'} ${this.props.possibleGrade}`}
</span>
);
}

View File

@@ -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}
/>
`;

View File

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

View File

@@ -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');

View File

@@ -5,12 +5,13 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is and empty and
body={
<div>
<ModalHeaders />
<StatusAlert
alertType="danger"
dialog="Weve been trying to contact you regarding..."
<Alert
dismissible={false}
open={true}
/>
show={true}
variant="danger"
>
Weve been trying to contact you regarding...
</Alert>
<OverrideTable />
<div>
<FormattedMessage
@@ -66,12 +67,13 @@ exports[`EditMoal Component snapshots gradeOverrideHistoryError is empty and ope
body={
<div>
<ModalHeaders />
<StatusAlert
alertType="danger"
dialog=""
<Alert
dismissible={false}
open={false}
/>
show={false}
variant="danger"
>
</Alert>
<OverrideTable />
<div>
<FormattedMessage

View File

@@ -6,7 +6,7 @@ import { connect } from 'react-redux';
import {
Button,
Modal,
StatusAlert,
Alert,
} from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
@@ -53,12 +53,13 @@ export class EditModal extends React.Component {
body={(
<div>
<ModalHeaders />
<StatusAlert
alertType="danger"
dialog={this.props.gradeOverrideHistoryError}
open={!!this.props.gradeOverrideHistoryError}
<Alert
variant="danger"
show={!!this.props.gradeOverrideHistoryError}
dismissible={false}
/>
>
{this.props.gradeOverrideHistoryError}
</Alert>
<OverrideTable />
<div><FormattedMessage {...messages.visibility} /></div>
<div><FormattedMessage {...messages.saveVisibility} /></div>

View File

@@ -16,7 +16,7 @@ jest.mock('./ModalHeaders', () => 'ModalHeaders');
jest.mock('@edx/paragon', () => ({
Button: () => 'Button',
Modal: () => 'Modal',
StatusAlert: () => 'StatusAlert',
Alert: () => 'Alert',
}));
jest.mock('data/actions', () => ({
__esModule: true,

View File

@@ -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>
`;

View File

@@ -3,8 +3,8 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { Table } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { DataTable } from '@edx/paragon';
import { FormattedMessage, getLocale, isRtl } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import { Headings } from 'data/constants/grades';
@@ -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) {
@@ -49,7 +50,7 @@ export class GradebookTable extends React.Component {
<Fields.Username username={entry.username} userKey={entry.external_user_key} />
),
[Headings.email]: (<Fields.Email email={entry.email} />),
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}%`,
[Headings.totalGrade]: `${roundGrade(entry.percent * 100)}${isRtl(getLocale()) ? '\u200f' : ''}%`,
};
entry.section_breakdown.forEach(subsection => {
dataRow[subsection.label] = (
@@ -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>
);
}

View File

@@ -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;

View File

@@ -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;

View File

@@ -31,7 +31,7 @@
.import-grades-btn {
margin-left: 20px;
}
.intervention-report-description: {
.intervention-report-description {
margin-right: 40px;
}
h4.step-message-1 {
@@ -67,104 +67,9 @@
overflow-x: auto;
height: 600px;
overflow-y: auto;
word-break: break-word;
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;

View File

@@ -18,13 +18,14 @@ import messages from './SearchControls.messages';
export class SearchControls extends React.Component {
constructor(props) {
super(props);
this.onChange = this.onChange.bind(this);
this.onBlur = this.onBlur.bind(this);
this.onClear = this.onClear.bind(this);
this.onSubmit = this.onSubmit.bind(this);
}
/** Changing the search value stores the key in Gradebook. Currently unused */
onChange(searchValue) {
this.props.setSearchValue(searchValue);
onBlur(e) {
this.props.setSearchValue(e.target.value);
}
onClear() {
@@ -32,13 +33,18 @@ export class SearchControls extends React.Component {
this.props.fetchGrades();
}
onSubmit(searchValue) {
this.props.setSearchValue(searchValue);
this.props.fetchGrades();
}
render() {
return (
<div>
<SearchField
onSubmit={this.props.fetchGrades}
onSubmit={this.onSubmit}
inputLabel={<FormattedMessage {...messages.label} />}
onChange={this.onChange}
onBlur={this.onBlur}
onClear={this.onClear}
value={this.props.searchValue}
/>

View File

@@ -4,7 +4,11 @@ import { shallow } from 'enzyme';
import selectors from 'data/selectors';
import actions from 'data/actions';
import thunkActions from 'data/thunkActions';
import { mapDispatchToProps, mapStateToProps, SearchControls } from './SearchControls';
import {
mapDispatchToProps,
mapStateToProps,
SearchControls,
} from './SearchControls';
jest.mock('@edx/paragon', () => ({
Icon: 'Icon',
@@ -15,7 +19,7 @@ jest.mock('data/selectors', () => ({
__esModule: true,
default: {
app: {
searchValue: jest.fn(state => ({ searchValue: state })),
searchValue: jest.fn((state) => ({ searchValue: state })),
},
},
}));
@@ -52,26 +56,45 @@ describe('SearchControls', () => {
describe('Snapshots', () => {
test('basic snapshot', () => {
const wrapper = searchControls();
wrapper.instance().onChange = jest.fn().mockName('onChange');
wrapper.instance().onBlur = jest.fn().mockName('onBlur');
wrapper.instance().onClear = jest.fn().mockName('onClear');
wrapper.instance().onSubmit = jest.fn().mockName('onSubmit');
expect(wrapper.instance().render()).toMatchSnapshot();
});
});
describe('onChange', () => {
it('saves the changed search value to Gradebook state', () => {
const wrapper = searchControls();
wrapper.instance().onChange('bob');
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
describe('Behavior', () => {
describe('onBlur', () => {
it('saves the search value to Gradebook state but do not fetch grade', () => {
const wrapper = searchControls();
const event = {
target: {
value: 'bob',
},
};
wrapper.instance().onBlur(event);
expect(props.setSearchValue).toHaveBeenCalledWith('bob');
expect(props.fetchGrades).not.toHaveBeenCalled();
});
});
});
describe('onChange', () => {
it('sets search value to empty string and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.setSearchValue).toHaveBeenCalledWith('');
expect(props.fetchGrades).toHaveBeenCalled();
describe('onClear', () => {
it('sets search value to empty string and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onClear();
expect(props.setSearchValue).toHaveBeenCalledWith('');
expect(props.fetchGrades).toHaveBeenCalled();
});
});
describe('onSubmit', () => {
it('sets search value to input and calls fetchGrades', () => {
const wrapper = searchControls();
wrapper.instance().onSubmit('John');
expect(props.setSearchValue).toHaveBeenCalledWith('John');
expect(props.fetchGrades).toHaveBeenCalled();
});
});
});

View File

@@ -2,7 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { StatusAlert } from '@edx/paragon';
import { Alert } from '@edx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
@@ -40,18 +40,20 @@ export class StatusAlerts extends React.Component {
render() {
return (
<>
<StatusAlert
alertType="success"
dialog={<FormattedMessage {...messages.editSuccessAlert} />}
<Alert
variant="success"
onClose={this.props.handleCloseSuccessBanner}
open={this.props.showSuccessBanner}
/>
<StatusAlert
alertType="danger"
dialog={this.courseGradeFilterAlertDialogText}
show={this.props.showSuccessBanner}
>
<FormattedMessage {...messages.editSuccessAlert} />
</Alert>
<Alert
variant="danger"
dismissible={false}
open={this.isCourseGradeFilterAlertOpen}
/>
show={this.isCourseGradeFilterAlertOpen}
>
{this.courseGradeFilterAlertDialogText}
</Alert>
</>
);
}

View File

@@ -13,7 +13,7 @@ import {
} from './StatusAlerts';
jest.mock('@edx/paragon', () => ({
StatusAlert: 'StatusAlert',
Alert: 'Alert',
}));
jest.mock('data/selectors', () => ({
__esModule: true,

View File

@@ -10,9 +10,9 @@ exports[`SearchControls Component Snapshots basic snapshot 1`] = `
id="gradebook.GradesView.search.label"
/>
}
onChange={[MockFunction onChange]}
onBlur={[MockFunction onBlur]}
onClear={[MockFunction onClear]}
onSubmit={[MockFunction fetchGrades]}
onSubmit={[MockFunction onSubmit]}
value="alice"
/>
<small

View File

@@ -2,23 +2,23 @@
exports[`StatusAlerts snapshots basic snapshot 1`] = `
<React.Fragment>
<StatusAlert
alertType="success"
dialog={
<FormattedMessage
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
description="An alert text for successfully editing a grade"
id="gradebook.GradesView.editSuccessAlert"
/>
}
<Alert
onClose={[MockFunction handleCloseSuccessBanner]}
open={true}
/>
<StatusAlert
alertType="danger"
dialog="the quiCk brown does somEthing or other"
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="The grade has been successfully edited. You may see a slight delay before updates appear in the Gradebook."
description="An alert text for successfully editing a grade"
id="gradebook.GradesView.editSuccessAlert"
/>
</Alert>
<Alert
dismissible={false}
open={false}
/>
show={false}
variant="danger"
>
the quiCk brown does somEthing or other
</Alert>
</React.Fragment>
`;

View File

@@ -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',
},

View File

@@ -131,7 +131,7 @@ describe('app reducer', () => {
const mockDate = new Date(8675309);
let dateSpy;
beforeEach(() => {
dateSpy = jest.spyOn(global, 'Date').mockReturnValue(mockDate);
dateSpy = jest.spyOn(global, 'Date').mockImplementation(() => mockDate);
});
afterEach(() => {
dateSpy.mockRestore();

View File

@@ -3,6 +3,7 @@ import { StrictDict } from 'utils';
import { Headings, GradeFormats } from 'data/constants/grades';
import { formatDateForDisplay } from 'data/actions/utils';
import { getLocale, isRtl } from '@edx/frontend-platform/i18n';
import simpleSelectorFactory from '../utils';
import * as module from './grades';
@@ -156,7 +157,7 @@ export const subsectionGrade = StrictDict({
[GradeFormats.absolute]: (subsection) => {
const earned = module.roundGrade(subsection.score_earned);
const possible = module.roundGrade(subsection.score_possible);
return subsection.attempted ? `${earned}/${possible}` : `${earned}`;
return subsection.attempted ? `${earned}${isRtl(getLocale()) ? '\\' : '/'}${possible}` : `${earned}`;
},
/**
* subsectionGrade.percent(subsection)

187
src/testUtils.js Normal file
View 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));
});
}
}

View File

@@ -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];
}

View File

@@ -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