Compare commits

..

2 Commits

Author SHA1 Message Date
obscherler
78dd2c5e61 fix: Update frontend-platform to version 8.3.8 for fixes with theme URL loading. (#521)
8.3.7: Simplify the logic for Paragon `fallbackThemeUrl` to always rely on `window.location?.origin`;
8.3.8: Allow the creation of URLs with only `brandOverride` definition.
2025-11-06 09:49:12 -05:00
Brian Smith
0171359f34 feat!: add design tokens support (#470)
BREAKING CHANGE: Pre-design-tokens theming is no longer supported.

Co-authored-by: Diana Olarte <diana.olarte@edunext.co>
2025-07-30 12:56:00 -05:00
112 changed files with 6288 additions and 6939 deletions

6
.github/CODEOWNERS vendored Normal file
View File

@@ -0,0 +1,6 @@
# Code owners for frontend-app-gradebook, editable gradebook micro-frontend (MFE)
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, they will
# be requested for review when someone opens a pull request.
* @openedx/content-aurora

View File

@@ -47,7 +47,7 @@ jobs:
- name: Send failure notification
if: ${{ failure() }}
uses: dawidd6/action-send-mail@v6
uses: dawidd6/action-send-mail@v4
with:
server_address: email-smtp.us-east-1.amazonaws.com
server_port: 465

2
.nvmrc
View File

@@ -1 +1 @@
24
20

189
README.md Executable file
View File

@@ -0,0 +1,189 @@
# 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)
[![Codecov](https://img.shields.io/codecov/c/gh/openedx/frontend-app-gradebook)](https://app.codecov.io/gh/openedx/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)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
# Purpose
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
Jump to:
- [Should I use Gradebook in my course?](#should-i-use-gradebook-in-my-course)
- [Quickstart](#quickstart)
For existing documentation see:
- Basic Usage: [Review Learner Grades (read-the-docs)](https://docs.openedx.org/en/latest/educators/how-tos/data/view_learner_grades.html)
- Bulk Grade Management: [Override Learner Subsection Scores in Bulk (read-the-docs)](https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#override-learner-subsection-scores-in-bulk)
## Should I use Gradebook in my course?
### What does this offer over the legacy gradebook?
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
scored within a certain range, and by assignment types (note: Not problem types, but categories like Exams or
Homework).
### What does the legacy gradebook offer that this project does not?
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
quick links to the problems for the instructor to visit. It expects the instructor to be familiar with the problems they
are grading and which unit they refer to.
The gradebook is expected to be much more performant for larger numbers of students as well. The Instructor Dashboard
link for the legacy gradebook reports that "this feature is available only to courses with a small number of enrolled
learners." However, this project comes with no such warning.
### Who should not change to this gradebook?
Groups whose instructors need not ever manually override grades do not need this project, but may not be any worse off
depending on their needs. Instructors that expect to review grades infrequently enough that not having a direct link
to the problem in question will have a worse UX than the legacy gradebook provides. Instructors that rely on the graphs
generated by the current gradebook might find the lack of autogenerated graphs to be frustrating.
## Getting Started
### Installation
To install gradebook into your project:
```
npm i --save @edx/frontend-app-gradebook
```
Cloning and Startup
===================
1. Clone your new repo:
``git clone https://github.com/openedx/frontend-app-gradebook.git``
2. Use the version of Node specified in ``.nvmrc``
3. Stop the Tutor devstack, if it's running:
``tutor dev stop``
4. Next, we need to tell Tutor that we're going to be running this repo in development mode, and it should be excluded from the mfe container that otherwise runs every MFE. Run this:
``tutor mounts add /path/to/frontend-app-gradebook``
5. Start Tutor in development mode. This command will start the LMS and Studio,
and other required MFEs like ``authn`` and ``account``, but will not start the
Gradebook MFE, which we're going to run on the host instead of in a container
managed by Tutor. Run:
``tutor dev start lms cms mfe``
## Startup
1. Install npm dependencies:
``cd frontend-app-gradebook && npm install``
2. Start the dev server:
``npm run dev``
## Running the UI Standalone
To install the project please refer to the [`MFE Development on Tutor`](https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development) instructions.
When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
When mounted in the tutor ``gradebook`` container, or when running a local (host) webpack dev server, the web application runs on port **1994**, so when you go to `http://apps.local.openedx.io:1994/gradebook/course-v1:edX+DemoX+Demo_Course` you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
(Note: This may not work in Tutor; these instructions are for the deprecated Devstack) You can see the log messages for the docker container by executing `make gradebook-logs` in the `devstack` directory.
Note that starting the container executes the `npm run start` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
## Plugins
This MFE can be customized using [Frontend Plugin Framework](https://github.com/openedx/frontend-plugin-framework).
The parts of this MFE that can be customized in that manner are documented [here](/src/plugin-slots).
## Running tests
Run:
``nvm use``
``npm ci``
``npm test``
## Directory Structure
* `config`
* Directory for [`webpack`](https://webpack.js.org/) configurations
* `public`
* Entry point for the single-page application - `gradebook` has a single `index.html` file
* `src`
* `components`
* Directory for presentational `React` components
* `containers`
* Directory for container `React` components
* `data`
* `actions`
* Directory for `Redux` action creators
* `constants`
* `reducers`
* Directory for `Redux` reducers
## Authentication with backend API services
See the [`@edx/frontend-auth`](https://github.com/edx-unsupported/frontend-auth) repo for information about securing routes in your application that require user authentication.
License
=======
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Contributing
============
Contributions are very welcome. Please read [How To Contribute](https://docs.openedx.org/en/latest/developers/references/developer_guide/process/index.html) for details.
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
===========
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a [Slack
invitation](https://openedx.org/slack), then join our
[community Slack workspace](https://openedx.slack.com/) Because this is a
frontend repository, the best place to discuss it would be in the
[#wg-frontend channel](https://openedx.slack.com/archives/C04BM6YC7A6).
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-gradebook/issues
For more information about these options, see the [Getting Help](https://openedx.org/community/connect) page.
The Open edX Code of Conduct
============================
All community members are expected to follow the [Open edX Code of Conduct](https://openedx.org/code-of-conduct/).
Reporting Security Issues
=========================
Please do not report security issues in public. Please email security@openedx.org.

View File

@@ -1,253 +0,0 @@
frontend-app-gradebook
#######################
|Build Status| |Codecov| |npm_version| |npm_downloads| |license| |semantic-release|
Purpose
*******
Gradebook allows course staff to view, filter, and override subsection grades for a course. Additionally for Masters courses, Gradebook enables bulk management of subsection grades.
Jump to:
- `Should I use Gradebook in my course?`_
- Quickstart_
For existing documentation see:
- Basic Usage: `Review Learner Grades (read-the-docs) <https://docs.openedx.org/en/latest/educators/how-tos/data/view_learner_grades.html>`_
- Bulk Grade Management: `Override Learner Subsection Scores in Bulk (read-the-docs) <https://docs.openedx.org/en/latest/educators/how-tos/data/manage_learner_grades.html#override-learner-subsection-scores-in-bulk>`_
Should I use Gradebook in my course?
*************************************
What does this offer over the legacy gradebook?
================================================
The micro-frontend offers a great deal more granularity when searching for problems, an easy interface for editing grades, an
audit trail for seeing who edited what grade and what reason they gave (if any) for doing so.
UsageProblems can be filtered by student as in the traditional gradebook, but can also be filtered by scores to see who
scored within a certain range, and by assignment types (note: Not problem types, but categories like 'Exams' or
'Homework').
What does the legacy gradebook offer that this project does not?
=================================================================
This project does not (yet, at least) create any graphs, which the traditional gradebook does. It also does not give
quick links to the problems for the instructor to visit. It expects the instructor to be familiar with the problems they
are grading and which unit they refer to.
The gradebook is expected to be much more performant for larger numbers of students as well. The Instructor Dashboard
link for the legacy gradebook reports that "this feature is available only to courses with a small number of enrolled
learners." However, this project comes with no such warning.
Who should not change to this gradebook?
=========================================
Groups whose instructors need not ever manually override grades do not need this project, but may not be any worse off
depending on their needs. Instructors that expect to review grades infrequently enough that not having a direct link
to the problem in question will have a worse UX than the legacy gradebook provides. Instructors that rely on the graphs
generated by the current gradebook might find the lack of autogenerated graphs to be frustrating.
Getting Started
***************
Installation
============
To install gradebook into your project:
.. code-block:: bash
npm i --save @edx/frontend-app-gradebook
Quickstart
==========
Cloning and Setup
-----------------
1. Clone your new repo:
.. code-block:: bash
git clone https://github.com/openedx/frontend-app-gradebook.git
2. Use the version of Node specified in ``.nvmrc``
3. Stop the Tutor devstack, if it's running:
.. code-block:: bash
tutor dev stop
4. Next, we need to tell Tutor that we're going to be running this repo in development mode, and it should be excluded from the mfe container that otherwise runs every MFE. Run this:
.. code-block:: bash
tutor mounts add /path/to/frontend-app-gradebook
5. Start Tutor in development mode. This command will start the LMS and Studio,
and other required MFEs like ``authn`` and ``account``, but will not start the
Gradebook MFE, which we're going to run on the host instead of in a container
managed by Tutor. Run:
.. code-block:: bash
tutor dev start lms cms mfe
Startup
-------
1. Install npm dependencies:
.. code-block:: bash
cd frontend-app-gradebook && npm install
2. Start the dev server:
.. code-block:: bash
npm run dev
Running the UI Standalone
==========================
To install the project please refer to the `MFE Development on Tutor <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`_ instructions.
When not mounted, gradebook will run in the shared MFE container at http://apps.local.openedx.io/gradebook/course-v1:edX+DemoX+Demo_Course.
When mounted in the tutor ``gradebook`` container, or when running a local (host) webpack dev server, the web application runs on port **1994**, so when you go to http://apps.local.openedx.io:1994/gradebook/course-v1:edX+DemoX+Demo_Course you should see the UI (assuming you have such a Demo Course in your devstack). Note that you always have to provide a course id to actually see a gradebook.
(Note: This may not work in Tutor; these instructions are for the deprecated Devstack) You can see the log messages for the docker container by executing ``make gradebook-logs`` in the ``devstack`` directory.
Note that starting the container executes the ``npm run start`` script which will hot-reload JavaScript and Sass files changes, so you should (:crossed_fingers:) not need to do anything (other than wait) when making changes.
Plugins
*******
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
Running tests
*************
Run:
.. code-block:: bash
nvm use
npm ci
npm test
Directory Structure
*******************
* ``config``
* Directory for `webpack <https://webpack.js.org/>`_ configurations
* ``public``
* Entry point for the single-page application - ``gradebook`` has a single ``index.html`` file
* ``src``
* ``components``
* Directory for presentational ``React`` components
* ``containers``
* Directory for container ``React`` components
* ``data``
* ``actions``
* Directory for ``Redux`` action creators
* ``constants``
* ``reducers``
* Directory for ``Redux`` reducers
Authentication with backend API services
*****************************************
See the `@edx/frontend-auth <https://github.com/edx-unsupported/frontend-auth>`_ repo for information about securing routes in your application that require user authentication.
License
*******
The code in this repository is licensed under the AGPLv3 unless otherwise
noted.
Please see `LICENSE <LICENSE>`_ for details.
Contributing
************
Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure
to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing
your idea.
Getting Help
************
If you're having trouble, we have discussion forums at
https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a
frontend repository, the best place to discuss it would be in the
`#wg-frontend channel`_.
For anything non-trivial, the best path is to open an issue in this repository
with as many details about the issue you are facing as you can provide.
https://github.com/openedx/frontend-app-gradebook/issues
For more information about these options, see the `Getting Help`_ page.
.. _Slack invitation: https://openedx.org/slack
.. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect
The Open edX Code of Conduct
*****************************
All community members are expected to follow the `Open edX Code of Conduct`_.
.. _Open edX Code of Conduct: https://openedx.org/code-of-conduct/
Reporting Security Issues
**************************
Please do not report security issues in public. Please email security@openedx.org.
.. |Build Status| image:: https://api.travis-ci.com/edx/frontend-app-gradebook.svg?branch=master
:target: https://travis-ci.com/edx/frontend-app-gradebook
.. |Codecov| image:: https://img.shields.io/codecov/c/gh/openedx/frontend-app-gradebook
:target: https://app.codecov.io/gh/openedx/frontend-app-gradebook
.. |npm_version| image:: https://img.shields.io/npm/v/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |npm_downloads| image:: https://img.shields.io/npm/dt/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |license| image:: https://img.shields.io/npm/l/@edx/frontend-app-gradebook.svg
:target: @edx/frontend-app-gradebook
.. |semantic-release| image:: https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg
:target: https://github.com/semantic-release/semantic-release

View File

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

6368
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,6 @@
{
"name": "@edx/frontend-app-gradebook",
"version": "1.6.3",
"version": "1.6.2",
"description": "edx editable gradebook-ui to manipulate grade overrides on subsections",
"repository": {
"type": "git",
@@ -31,9 +31,11 @@
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/frontend-component-footer": "^14.6.0",
"@edx/frontend-component-header": "^6.6.1",
"@edx/frontend-platform": "^8.3.7",
"@edx/frontend-component-header": "^6.4.0",
"@edx/frontend-platform": "^8.3.8",
"@edx/openedx-atlas": "^0.6.0",
"@edx/react-unit-test-utils": "^4.0.0",
"@edx/reactifex": "^2.1.1",
"@fortawesome/fontawesome-svg-core": "^1.2.25",
"@fortawesome/free-brands-svg-icons": "^5.11.2",
"@fortawesome/free-solid-svg-icons": "^5.11.2",
@@ -67,16 +69,15 @@
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.6.4",
"@testing-library/react": "^16.3.0",
"@testing-library/user-event": "^14.6.1",
"@openedx/frontend-build": "^14.3.3",
"@testing-library/react": "^16.2.0",
"es-check": "^2.3.0",
"fetch-mock": "^12.2.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"react-dev-utils": "^12.0.1",
"react-test-renderer": "^18.3.1",
"reactifex": "1.1.1",
"redux-mock-store": "^1.5.3"
}
}

View File

@@ -1,63 +1,63 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { Route } from 'react-router-dom';
import store from 'data/store';
import GradebookPage from 'containers/GradebookPage';
import App from './App';
jest.mock('react-router-dom', () => ({
Routes: ({ children }) => children,
Route: ({ element }) => element,
BrowserRouter: () => 'BrowserRouter',
Route: () => 'Route',
Routes: () => 'Routes',
}));
jest.mock('@edx/frontend-platform/react', () => ({
AppProvider: ({ children }) => children,
AppProvider: () => 'AppProvider',
}));
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
jest.mock('data/store', () => 'testStore');
jest.mock('containers/GradebookPage', () => 'GradebookPage');
jest.mock('@edx/frontend-component-header', () => 'Header');
jest.mock('./head/Head', () => 'Head');
jest.mock('@edx/frontend-component-header', () => ({
__esModule: true,
default: () => <div>Header</div>,
}));
let el;
let secondChild;
jest.mock('@edx/frontend-component-footer', () => ({
FooterSlot: () => <div>Footer</div>,
}));
jest.mock('./head/Head', () => ({
__esModule: true,
default: () => <div>Head</div>,
}));
jest.mock('containers/GradebookPage', () => ({
__esModule: true,
default: () => <div>Gradebook</div>,
}));
describe('App', () => {
beforeEach(() => {
render(<App />);
describe('App router component', () => {
test('snapshot', () => {
expect(shallow(<App />).snapshot).toMatchSnapshot();
});
afterEach(() => {
jest.clearAllMocks();
});
it('renders Head component', () => {
const head = screen.getByText('Head');
expect(head).toBeInTheDocument();
});
it('renders Header component', () => {
const header = screen.getByText('Header');
expect(header).toBeInTheDocument();
});
it('renders Footer component', () => {
const footer = screen.getByText('Footer');
expect(footer).toBeInTheDocument();
});
it('renders main content wrapper', () => {
const main = screen.getByRole('main');
expect(main).toBeInTheDocument();
const gradebook = screen.getByText('Gradebook');
expect(gradebook).toBeInTheDocument();
describe('component', () => {
beforeEach(() => {
el = shallow(<App />);
secondChild = el.instance.children;
});
describe('AppProvider', () => {
test('AppProvider is the parent component, passed the redux store props', () => {
expect(el.instance.type).toBe('AppProvider');
expect(el.instance.props.store).toEqual(store);
});
});
describe('Head', () => {
test('first child of AppProvider', () => {
expect(el.instance.children[0].type).toBe('Head');
});
});
describe('Router', () => {
test('second child of AppProvider', () => {
expect(secondChild[1].type).toBe('div');
});
test('Header is above/outside-of the routing', () => {
expect(secondChild[1].children[0].type).toBe('Header');
expect(secondChild[1].children[1].type).toBe('main');
});
test('Routing - GradebookPage is only route', () => {
expect(secondChild[1].findByType(Route)).toHaveLength(1);
expect(secondChild[1].findByType(Route)[0].props.path).toEqual('/:courseId');
expect(secondChild[1].findByType(Route)[0].props.element.type).toEqual(GradebookPage);
});
});
});
});

View File

@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`App router component snapshot 1`] = `
<AppProvider
store="testStore"
>
<Head />
<div>
<Header />
<main>
<Routes>
<Route
element={<GradebookPage />}
path="/:courseId"
/>
</Routes>
</main>
<FooterSlot />
</div>
</AppProvider>
`;

View File

@@ -1,10 +1,20 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import selectors from 'data/selectors';
import messages from './messages';
import { BulkManagementAlerts, mapStateToProps } from './BulkManagementAlerts';
import { renderWithIntl, screen } from '../../testUtilsExtra';
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('@openedx/paragon', () => ({
Alert: () => 'Alert',
}));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
@@ -19,19 +29,47 @@ const errorMessage = 'Oh noooooo';
describe('BulkManagementAlerts', () => {
describe('component', () => {
describe('states of the warnings', () => {
test('no alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess={false} />);
expect(document.querySelectorAll('.alert').length).toEqual(0);
let el;
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts />);
});
test('Just success alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError="" uploadSuccess />);
expect(document.querySelectorAll('.alert-success').length).toEqual(1);
test('snapshot - bulkImportError closed, success closed', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Just error alert shown', () => {
renderWithIntl(<BulkManagementAlerts bulkImportError={errorMessage} uploadSuccess={false} />);
expect(document.querySelectorAll('.alert-danger').length).toEqual(1);
expect(screen.getByText(errorMessage)).toBeInTheDocument();
test('closed danger alert', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[0].props.variant).toEqual('danger');
});
test('closed success alert', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.instance.findByType(Alert)[1].props.show).toEqual(false);
expect(el.instance.findByType(Alert)[1].props.variant).toEqual('success');
});
});
describe('no errer, no upload success', () => {
beforeEach(() => {
el = shallow(<BulkManagementAlerts uploadSuccess bulkImportError={errorMessage} />);
});
const assertions = [
'danger alert open with bulkImportError',
'success alert open with messages.successDialog',
];
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
});
test('open danger alert with bulkImportError content', () => {
expect(el.instance.children[0].type).toBe('Alert');
expect(el.instance.findByType(Alert)[0].children[0].el).toEqual(errorMessage);
expect(el.instance.findByType(Alert)[0].props.show).toEqual(true);
});
test('open success alert with messages.successDialog content', () => {
expect(el.instance.children[1].type).toBe('Alert');
expect(el.shallowWrapper.props.children[1].props.children).toEqual(
<FormattedMessage {...messages.successDialog} />,
);
expect(el.instance.children[1].props.show).toEqual(true);
});
});
});

View File

@@ -1,187 +1,108 @@
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/paragon';
import selectors from 'data/selectors';
import { bulkManagementColumns } from 'data/constants/app';
import { HistoryTable, mapHistoryRows, mapStateToProps } from './HistoryTable';
import ResultsSummary from './ResultsSummary';
import { HistoryTable, mapStateToProps } from './HistoryTable';
initializeMocks();
jest.mock('@openedx/paragon', () => ({ DataTable: () => 'DataTable' }));
jest.mock('@openedx/paragon', () => ({
...jest.requireActual('@openedx/paragon'),
DataTable: jest.fn(() => <div data-testid="data-table">DataTable</div>),
jest.mock('@edx/frontend-platform/i18n', () => ({
defineMessages: m => m,
FormattedMessage: () => 'FormattedMessage',
}));
jest.mock('./ResultsSummary', () => jest.fn(() => <div data-testid="results-summary">ResultsSummary</div>));
jest.mock('data/selectors', () => ({
__esModule: true,
default: {
grades: {
bulkManagementHistoryEntries: jest.fn(),
bulkManagementHistoryEntries: jest.fn(state => ({ historyEntries: state })),
},
},
}));
jest.mock('./ResultsSummary', () => 'ResultsSummary');
describe('HistoryTable', () => {
beforeEach(() => {
jest.clearAllMocks();
});
const mockBulkManagementHistory = [
{
originalFilename: 'test-file-1.csv',
user: 'test-user-1',
timeUploaded: '2025-01-01T10:00:00Z',
resultsSummary: {
rowId: 1,
text: 'Download results 1',
},
},
{
originalFilename: 'test-file-2.csv',
user: 'test-user-2',
timeUploaded: '2025-01-02T10:00:00Z',
resultsSummary: {
rowId: 2,
text: 'Download results 2',
},
},
];
beforeEach(() => {
jest.clearAllMocks();
});
describe('mapHistoryRows', () => {
const mockRow = {
resultsSummary: {
rowId: 1,
text: 'Download results',
},
originalFilename: 'test-file.csv',
user: 'test-user',
timeUploaded: '2025-01-01T10:00:00Z',
};
it('transforms row data correctly', () => {
const result = mapHistoryRows(mockRow);
expect(result).toHaveProperty('resultsSummary');
expect(result).toHaveProperty('filename');
expect(result).toHaveProperty('user');
expect(result).toHaveProperty('timeUploaded');
expect(result.timeUploaded).toBe('2025-01-01T10:00:00Z');
});
it('wraps filename in span with correct class', () => {
const result = mapHistoryRows(mockRow);
render(<div>{result.filename}</div>);
const filenameSpan = screen.getByText('test-file.csv');
expect(filenameSpan).toBeInTheDocument();
expect(filenameSpan).toHaveClass('wrap-text-in-cell');
});
it('wraps user in span with correct class', () => {
const result = mapHistoryRows(mockRow);
render(<div>{result.user}</div>);
const userSpan = screen.getByText('test-user');
expect(userSpan).toBeInTheDocument();
expect(userSpan).toHaveClass('wrap-text-in-cell');
});
it('renders ResultsSummary component with correct props', () => {
const result = mapHistoryRows(mockRow);
render(<div>{result.resultsSummary}</div>);
expect(ResultsSummary).toHaveBeenCalledWith(mockRow.resultsSummary, {});
expect(screen.getByTestId('results-summary')).toBeInTheDocument();
});
});
describe('component', () => {
it('renders DataTable with empty data when no history provided', () => {
render(<HistoryTable />);
expect(DataTable).toHaveBeenCalledWith(
{
data: [],
hasFixedColumnWidths: true,
columns: bulkManagementColumns,
className: 'table-striped',
itemCount: 0,
},
{},
);
expect(screen.getByTestId('data-table')).toBeInTheDocument();
});
it('renders DataTable with mapped history data', () => {
render(
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
);
expect(DataTable).toHaveBeenCalledWith(
{
data: expect.arrayContaining([
expect.objectContaining({
filename: expect.any(Object),
user: expect.any(Object),
resultsSummary: expect.any(Object),
timeUploaded: '2025-01-01T10:00:00Z',
}),
expect.objectContaining({
filename: expect.any(Object),
user: expect.any(Object),
resultsSummary: expect.any(Object),
timeUploaded: '2025-01-02T10:00:00Z',
}),
]),
hasFixedColumnWidths: true,
columns: bulkManagementColumns,
className: 'table-striped',
itemCount: 2,
},
{},
);
});
it('passes correct props to DataTable', () => {
render(
<HistoryTable bulkManagementHistory={mockBulkManagementHistory} />,
);
const dataTableCall = DataTable.mock.calls[0][0];
expect(dataTableCall.hasFixedColumnWidths).toBe(true);
expect(dataTableCall.columns).toBe(bulkManagementColumns);
expect(dataTableCall.className).toBe('table-striped');
expect(dataTableCall.itemCount).toBe(mockBulkManagementHistory.length);
const entry1 = {
originalFilename: 'blue.png',
user: 'Eifel',
timeUploaded: '65',
resultsSummary: {
rowId: 12,
courseId: 'Da Bu Dee',
text: 'Da ba daa',
},
};
const entry2 = {
originalFilename: 'allStar.jpg',
user: 'Smashmouth',
timeUploaded: '2000s?',
resultsSummary: {
courseId: 'rockstar',
rowId: 2,
text: 'all that glitters is gold',
},
};
const props = {
bulkManagementHistory: [entry1, entry2],
};
let el;
describe('snapshot', () => {
beforeEach(() => {
el = shallow(<HistoryTable {...props} />);
});
test('snapshot - loads formatted table', () => {
expect(el.snapshot).toMatchSnapshot();
});
describe('history table', () => {
let table;
beforeEach(() => {
table = el.instance.findByType(DataTable);
});
describe('data (from bulkManagementHistory.map(this.formatHistoryRow)', () => {
const fieldAssertions = [
'maps resultsSummay to ResultsSummary',
'wraps filename and user',
'forwards the rest',
];
test(`snapshot: ${fieldAssertions.join(', ')}`, () => {
expect(table[0].props.data).toMatchSnapshot();
});
test(fieldAssertions.join(', '), () => {
const rows = table[0].props.data;
expect(rows[0].resultsSummary).toEqual(<ResultsSummary {...entry1.resultsSummary} />);
expect(rows[0].user).toEqual(<span className="wrap-text-in-cell">{entry1.user}</span>);
expect(
rows[0].filename,
).toEqual(<span className="wrap-text-in-cell">{entry1.originalFilename}</span>);
expect(rows[1].resultsSummary).toEqual(<ResultsSummary {...entry2.resultsSummary} />);
expect(rows[1].user).toEqual(<span className="wrap-text-in-cell">{entry2.user}</span>);
expect(
rows[1].filename,
).toEqual(<span className="wrap-text-in-cell">{entry2.originalFilename}</span>);
});
});
test('columns from bulkManagementColumns', () => {
expect(table[0].props.columns).toEqual(bulkManagementColumns);
});
});
});
});
describe('mapStateToProps', () => {
const mockState = { test: 'state' };
const mockHistoryEntries = [
{ originalFilename: 'file1.csv', user: 'user1' },
{ originalFilename: 'file2.csv', user: 'user2' },
];
const testState = { a: 'simple', test: 'state' };
let mapped;
beforeEach(() => {
selectors.grades.bulkManagementHistoryEntries.mockReturnValue(
mockHistoryEntries,
);
mapped = mapStateToProps(testState);
});
it('maps bulkManagementHistory from selector', () => {
const result = mapStateToProps(mockState);
test('bulkManagementHistory from grades.bulkManagementHistoryEntries', () => {
expect(
selectors.grades.bulkManagementHistoryEntries,
).toHaveBeenCalledWith(mockState);
expect(result.bulkManagementHistory).toBe(mockHistoryEntries);
mapped.bulkManagementHistory,
).toEqual(selectors.grades.bulkManagementHistoryEntries(testState));
});
});
});

View File

@@ -19,8 +19,10 @@ const ResultsSummary = ({
text,
}) => (
<Hyperlink
destination={lms.urls.bulkGradesUrlByRow(rowId)}
href={lms.urls.bulkGradesUrlByRow(rowId)}
destination="www.edx.org"
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon src={Download} className="d-inline-block" />

View File

@@ -1,12 +1,21 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Download } from '@openedx/paragon/icons';
import lms from 'data/services/lms';
import { renderWithIntl, screen } from '../../testUtilsExtra';
import ResultsSummary from './ResultsSummary';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
Icon: () => 'Icon',
}));
jest.mock('@openedx/paragon/icons', () => ({
Download: 'DownloadIcon',
}));
jest.mock('data/services/lms', () => ({
urls: {
bulkGradesUrlByRow: jest.fn((rowId) => (`www.edx.org/${rowId}`)),
bulkGradesUrlByRow: jest.fn((rowId) => ({ url: { rowId } })),
},
}));
@@ -15,21 +24,29 @@ describe('ResultsSummary component', () => {
rowId: 42,
text: 'texty',
};
let link;
let el;
const assertions = [
'safe hyperlink with bulkGradesUrl with course and row id',
'download icon',
'results text',
];
beforeEach(() => {
renderWithIntl(<ResultsSummary {...props} />);
link = screen.getByRole('link', { name: props.text });
el = shallow(<ResultsSummary {...props} />);
});
test(`snapshot - ${assertions.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Hyperlink has target="_blank" and rel="noopener noreferrer"', () => {
expect(link).toHaveAttribute('target', '_blank');
expect(link).toHaveAttribute('rel', 'noopener noreferrer');
expect(el.instance.props.target).toEqual('_blank');
expect(el.instance.props.rel).toEqual('noopener noreferrer');
});
test('Hyperlink has href to bulkGradesUrl', () => {
expect(link).toHaveAttribute('href', lms.urls.bulkGradesUrlByRow(props.rowId));
expect(el.instance.props.href).toEqual(lms.urls.bulkGradesUrlByRow(props.rowId));
});
test('displays Download Icon and text', () => {
expect(link).toHaveTextContent(props.text);
const icon = screen.getByRole('img', { hidden: true });
expect(icon).toBeInTheDocument();
const icon = el.instance.children[0];
expect(icon.type).toEqual('Icon');
expect(icon.props.src).toEqual(Download);
expect(el.instance.children[1].el).toEqual(props.text);
});
});

View File

@@ -0,0 +1,45 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementAlerts component no errer, no upload success snapshot - bulkImportError closed, success closed 1`] = `
<Fragment>
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<Alert
dismissible={false}
show={false}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;
exports[`BulkManagementAlerts component no errer, no upload success snapshot - danger alert open with bulkImportError, success alert open with messages.successDialog 1`] = `
<Fragment>
<Alert
dismissible={false}
show={true}
variant="danger"
>
Oh noooooo
</Alert>
<Alert
dismissible={false}
show={true}
variant="success"
>
<FormattedMessage
defaultMessage="CSV processing. File uploads may take several minutes to complete."
description="Success Dialog message in BulkManagement Tab File Upload Form"
id="gradebook.BulkManagementHistoryView.successDialog"
/>
</Alert>
</Fragment>
`;

View File

@@ -0,0 +1,118 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryTable component snapshot history table data (from bulkManagementHistory.map(this.formatHistoryRow) snapshot: maps resultsSummay to ResultsSummary, wraps filename and user, forwards the rest 1`] = `
[
{
"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>,
},
{
"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>,
},
]
`;
exports[`HistoryTable component snapshot snapshot - loads formatted table 1`] = `
<DataTable
className="table-striped"
columns={
[
{
"Header": "Gradebook",
"accessor": "filename",
"columnSortable": false,
"width": "col-5",
},
{
"Header": "Download Summary",
"accessor": "resultsSummary",
"columnSortable": false,
"width": "col",
},
{
"Header": "Who",
"accessor": "user",
"columnSortable": false,
"width": "col-1",
},
{
"Header": "When",
"accessor": "timeUploaded",
"columnSortable": false,
"width": "col",
},
]
}
data={
[
{
"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>,
},
{
"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

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ResultsSummary component snapshot - safe hyperlink with bulkGradesUrl with course and row id, download icon, results text 1`] = `
<Hyperlink
destination="www.edx.org"
href={
{
"url": {
"rowId": 42,
},
}
}
rel="noopener noreferrer"
showLaunchIcon={false}
target="_blank"
>
<Icon
className="d-inline-block"
src="DownloadIcon"
/>
texty
</Hyperlink>
`;

View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementHistoryView component snapshot snapshot - loads heading from messages.BulkManagementHistoryView.heading, <BulkManagementAlerts />, <HistoryTable /> 1`] = `
<div
className="bulk-management-history-view"
>
<h4>
<FormattedMessage
defaultMessage="Bulk Management History"
description="Heading text for BulkManagement History Tab"
id="gradebook.BulkManagementHistoryView.heading"
/>
</h4>
<p
className="help-text"
>
<FormattedMessage
defaultMessage="Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override."
description="Bulk Management History View help text"
id="gradebook.BulkManagementHistoryView"
/>
</p>
<BulkManagementAlerts />
<HistoryTable />
</div>
`;

View File

@@ -1,24 +1,46 @@
import { render, initializeMocks, screen } from 'testUtilsExtra';
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { BulkManagementHistoryView } from '.';
import BulkManagementAlerts from './BulkManagementAlerts';
import HistoryTable from './HistoryTable';
import messages from './messages';
jest.mock('./BulkManagementAlerts', () => jest.fn(() => <div>BulkManagementAlerts</div>));
jest.mock('./HistoryTable', () => jest.fn(() => <div>HistoryTable</div>));
initializeMocks();
jest.mock('./BulkManagementAlerts', () => 'BulkManagementAlerts');
jest.mock('./HistoryTable', () => 'HistoryTable');
describe('BulkManagementHistoryView', () => {
describe('component', () => {
let el;
beforeEach(() => {
render(<BulkManagementHistoryView />);
el = shallow(<BulkManagementHistoryView />);
});
describe('render alerts and heading', () => {
it('heading - h4 loaded from messages', () => {
expect(screen.getByText(messages.heading.defaultMessage)).toBeInTheDocument();
expect(screen.getByText(messages.helpText.defaultMessage)).toBeInTheDocument();
expect(screen.getByText('BulkManagementAlerts')).toBeInTheDocument();
expect(screen.getByText('HistoryTable')).toBeInTheDocument();
describe('snapshot', () => {
const snapshotSegments = [
'heading from messages.BulkManagementHistoryView.heading',
'<BulkManagementAlerts />',
'<HistoryTable />',
];
test(`snapshot - loads ${snapshotSegments.join(', ')}`, () => {
expect(el.snapshot).toMatchSnapshot();
});
test('heading - h4 loaded from messages', () => {
const heading = el.instance.findByType('h4')[0];
const expectedHeading = shallow(
<h4>
<FormattedMessage {...messages.heading} />
</h4>,
);
expect(heading.el.type).toEqual(expectedHeading.type);
expect(heading.el.props).toEqual(expectedHeading.props);
});
test('heading, then alerts, then upload form, then table', () => {
expect(el.instance.children[0].type).toEqual('h4');
expect(el.instance.children[2].type).toEqual(BulkManagementAlerts);
expect(el.instance.children[3].type).toEqual(HistoryTable);
});
});
});

View File

@@ -8,7 +8,7 @@ const messages = defineMessages({
},
helpText: {
id: 'gradebook.BulkManagementHistoryView',
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
defaultMessage: 'Below is a log of previous grade imports. To download a CSV of your gradebook and import grades for override, return to the Gradebook. Please note, after importing grades, it may take a few seconds to process the override.',
description: 'Bulk Management History View help text',
},
successDialog: {

View File

@@ -1,35 +0,0 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';
import Header from '.';
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(),
}));
describe('Header', () => {
beforeEach(() => {
jest.clearAllMocks();
});
test('has edx link with logo url', () => {
const url = 'www.ourLogo.url';
const baseUrl = 'www.lms.url';
getConfig.mockReturnValue({ LOGO_URL: url, LMS_BASE_URL: baseUrl });
render(
<IntlProvider messages={{}} locale="en">
<Header />
</IntlProvider>,
);
const link = screen.getByRole('link');
const logo = screen.getByAltText('edX logo');
expect(link).toHaveAttribute('href', `${baseUrl}/dashboard`);
expect(logo).toHaveAttribute('src', url);
});
});

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Header snapshot - has edx link with logo url 1`] = `
<div
className="mb-3"
>
<header
className="d-flex justify-content-center align-items-center p-3 border-bottom-blue"
>
<Hyperlink
destination="undefined/dashboard"
>
<img
alt="edX logo"
height="30"
src="www.ourLogo.url"
width="60"
/>
</Hyperlink>
<div />
</header>
</div>
`;

View File

@@ -0,0 +1,21 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { getConfig } from '@edx/frontend-platform';
import Header from '.';
jest.mock('@openedx/paragon', () => ({
Hyperlink: () => 'Hyperlink',
}));
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
describe('Header', () => {
test('snapshot - has edx link with logo url', () => {
const url = 'www.ourLogo.url';
getConfig.mockReturnValue({ LOGO_URL: url });
expect(shallow(<Header />).snapshot).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,52 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilter component render snapshot 1`] = `
<div
className="student-filters"
>
<SelectGroup
disabled={false}
id="assignment"
label="Assignment"
onChange={[MockFunction]}
options={
[
<option
value=""
>
All
</option>,
<option
value="label1"
>
label1
:
sLabel1
</option>,
<option
value="label2"
>
label2
:
sLabel2
</option>,
<option
value="label3"
>
label3
:
sLabel3
</option>,
<option
value="label4"
>
label4
:
sLabel4
</option>,
]
}
value="test-label"
/>
</div>
`;

View File

@@ -1,9 +1,12 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
import useAssignmentFilterData from './hooks';
import AssignmentFilter from '.';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn();
@@ -22,16 +25,29 @@ useAssignmentFilterData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => {
beforeAll(() => {
initializeMocks();
render(<AssignmentFilter updateQueryParams={updateQueryParams} />);
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
expect(screen.getAllByRole('option')).toHaveLength(assignmentFilterOptions.length + 1); // +1 for the default option
expect(screen.getAllByRole('option')[assignmentFilterOptions.length]).toHaveTextContent(assignmentFilterOptions[assignmentFilterOptions.length - 1].label);
const { options } = el.instance.findByType(SelectGroup)[0].props;
expect(options.length).toEqual(5);
const testOption = assignmentFilterOptions[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testOption.label);
expect(optionProps.children.join(''))
.toEqual(`${testOption.label}: ${testOption.subsectionLabel}`);
});
});
});

View File

@@ -0,0 +1,67 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilter component render with selected assignment snapshot 1`] = `
<div
className="grade-filter-inputs"
>
<PercentGroup
disabled={false}
id="assignmentGradeMin"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
disabled={false}
id="assignmentGradeMax"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
<div
className="grade-filter-action"
>
<Button
disabled={false}
name="assignmentGradeMinMax"
type="submit"
variant="outline-secondary"
>
Apply
</Button>
</div>
</div>
`;
exports[`AssignmentFilter component render without selected assignment snapshot 1`] = `
<div
className="grade-filter-inputs"
>
<PercentGroup
disabled={true}
id="assignmentGradeMin"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
disabled={true}
id="assignmentGradeMax"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
<div
className="grade-filter-action"
>
<Button
disabled={true}
name="assignmentGradeMinMax"
type="submit"
variant="outline-secondary"
>
Apply
</Button>
</div>
</div>
`;

View File

@@ -1,15 +1,17 @@
/* eslint-disable import/no-extraneous-dependencies */
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import PercentGroup from '../PercentGroup';
import useAssignmentGradeFilterData from './hooks';
import AssignmentFilter from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = {
handleSubmit: jest.fn(),
handleChange: jest.fn(),
handleSetMax: jest.fn(),
handleSetMin: jest.fn(),
selectedAssignment: 'test-assignment',
@@ -20,39 +22,37 @@ useAssignmentGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilter component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
expect(useAssignmentGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
describe('with selected assignment', () => {
beforeEach(() => {
jest.clearAllMocks();
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('renders a PercentGroup for both Max and Min filters', async () => {
const user = userEvent.setup();
const minGradeInput = screen.getByRole('spinbutton', { name: /Min Grade/i });
const maxGradeInput = screen.getByRole('spinbutton', { name: /Max Grade/i });
expect(minGradeInput).toBeInTheDocument();
expect(maxGradeInput).toBeInTheDocument();
expect(minGradeInput).toBeEnabled();
expect(maxGradeInput).toBeEnabled();
await user.type(minGradeInput, '25');
expect(hookData.handleSetMin).toHaveBeenCalled();
await user.type(maxGradeInput, '50');
expect(hookData.handleSetMax).toHaveBeenCalled();
it('renders a PercentGroup for both Max and Min filters', () => {
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.assignmentGradeMin);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMin);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.assignmentGradeMax);
expect(props.disabled).toEqual(false);
expect(props.onChange).toEqual(hookData.handleSetMax);
});
it('renders a submit button', async () => {
const user = userEvent.setup();
const submitButton = screen.getByRole('button', { name: /Apply/ });
expect(submitButton).toBeInTheDocument();
expect(submitButton).not.toHaveAttribute('disabled');
await user.click(submitButton);
expect(hookData.handleSubmit).toHaveBeenCalled();
it('renders a submit button', () => {
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleSubmit);
});
});
describe('without selected assignment', () => {
@@ -61,13 +61,16 @@ describe('AssignmentFilter component', () => {
...hookData,
selectedAssignment: null,
});
renderWithIntl(<AssignmentFilter updateQueryParams={updateQueryParams} />);
el = shallow(<AssignmentFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('disables controls', () => {
const minGrade = screen.getByRole('spinbutton', { name: /Min Grade/ });
const maxGrade = screen.getByRole('spinbutton', { name: /Max Grade/ });
expect(minGrade).toHaveAttribute('disabled');
expect(maxGrade).toHaveAttribute('disabled');
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.disabled).toEqual(true);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.disabled).toEqual(true);
});
});
});

View File

@@ -0,0 +1,44 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AssignmentFilterType component render snapshot 1`] = `
<div
className="student-filters"
>
<SelectGroup
disabled={true}
id="assignment-types"
label="Assignment Types"
onChange={[MockFunction]}
options={
[
<option
value=""
>
All
</option>,
<option
value="test-type"
>
test-type
</option>,
<option
value="type1"
>
type1
</option>,
<option
value="type2"
>
type2
</option>,
<option
value="type3"
>
type3
</option>,
]
}
value="test-type"
/>
</div>
`;

View File

@@ -1,10 +1,12 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
import useAssignmentFilterTypeData from './hooks';
import AssignmentFilterType from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const handleChange = jest.fn();
@@ -19,15 +21,27 @@ useAssignmentFilterTypeData.mockReturnValue({
const updateQueryParams = jest.fn();
let el;
describe('AssignmentFilterType component', () => {
beforeAll(() => {
renderWithIntl(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
el = shallow(<AssignmentFilterType updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useAssignmentFilterTypeData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('filter options', () => {
const options = screen.getAllByRole('option');
expect(options.length).toEqual(5); // 4 types + "All Types"
expect(options[1]).toHaveTextContent(testType);
const { options } = el.instance.findByType(SelectGroup)[0].props;
expect(options.length).toEqual(5);
const optionProps = options[1].props;
expect(optionProps.value).toEqual(assignmentTypes[0]);
expect(optionProps.children).toEqual(testType);
});
});
});

View File

@@ -0,0 +1,63 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CourseFilter component render if disabled snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={true}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;
exports[`CourseFilter component render with selected assignment snapshot 1`] = `
<Fragment>
<div
className="grade-filter-inputs"
>
<PercentGroup
id="minimum-grade"
label="Min Grade"
onChange={[MockFunction]}
value={23}
/>
<PercentGroup
id="maximum-grade"
label="Max Grade"
onChange={[MockFunction]}
value={300}
/>
</div>
<div
className="grade-filter-action"
>
<Button
disabled={false}
variant="outline-secondary"
>
Apply
</Button>
</div>
</Fragment>
`;

View File

@@ -1,10 +1,13 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import PercentGroup from '../PercentGroup';
import useCourseGradeFilterData from './hooks';
import CourseFilter from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('../PercentGroup', () => 'PercentGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookData = {
@@ -24,37 +27,48 @@ useCourseGradeFilterData.mockReturnValue(hookData);
const updateQueryParams = jest.fn();
let el;
describe('CourseFilter component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useCourseGradeFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('with selected assignment', () => {
beforeEach(() => {
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('renders a PercentGroup for both Max and Min filters', () => {
expect(screen.getByRole('spinbutton', { name: 'Min Grade' })).toHaveValue(hookData.min.value);
expect(screen.getByRole('spinbutton', { name: 'Max Grade' })).toHaveValue(hookData.max.value);
let { props } = el.instance.findByType(PercentGroup)[0];
expect(props.value).toEqual(hookData.min.value);
expect(props.onChange).toEqual(hookData.min.onChange);
props = el.instance.findByType(PercentGroup)[1].props;
expect(props.value).toEqual(hookData.max.value);
expect(props.onChange).toEqual(hookData.max.onChange);
});
it('renders a submit button', () => {
expect(screen.getByRole('button', { name: 'Apply' })).toBeInTheDocument();
// Expect it to be enabled
expect(screen.getByRole('button', { name: 'Apply' })).not.toBeDisabled();
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(false);
expect(props.onClick).toEqual(hookData.handleApplyClick);
});
});
describe('if disabled', () => {
beforeEach(() => {
jest.clearAllMocks();
useCourseGradeFilterData.mockReturnValueOnce({ ...hookData, isDisabled: true });
renderWithIntl(<CourseFilter updateQueryParams={updateQueryParams} />);
el = shallow(<CourseFilter updateQueryParams={updateQueryParams} />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('disables submit', () => {
expect(screen.getByRole('button', { name: 'Apply' })).toBeDisabled();
const { props } = el.instance.findByType(Button)[0];
expect(props.disabled).toEqual(true);
});
});
});

View File

@@ -1,4 +1,5 @@
import { render, screen, initializeMocks } from 'testUtilsExtra';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import PercentGroup from './PercentGroup';
@@ -11,7 +12,6 @@ describe('PercentGroup', () => {
};
beforeEach(() => {
initializeMocks();
props = {
...props,
onChange: jest.fn().mockName('props.onChange'),
@@ -19,17 +19,15 @@ describe('PercentGroup', () => {
});
describe('Component', () => {
test('is displayed', () => {
render(<PercentGroup {...props} />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeInTheDocument();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
});
test('disabled', () => {
render(<PercentGroup {...props} disabled />);
expect(screen.getByRole('spinbutton', { name: 'Group Label' })).toBeDisabled();
expect(screen.getByText('Group Label')).toBeVisible();
expect(screen.getByText('%')).toBeVisible();
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<PercentGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<PercentGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import SelectGroup from './SelectGroup';
@@ -24,14 +24,15 @@ describe('SelectGroup', () => {
});
describe('Component', () => {
test('rendered with all options and label', () => {
render(<SelectGroup {...props} />);
expect(screen.getAllByRole('option')).toHaveLength(props.options.length);
expect(screen.getByLabelText(props.label)).toBeInTheDocument();
});
test('disabled', () => {
render(<SelectGroup {...props} disabled />);
expect(screen.getByRole('combobox')).toBeDisabled();
describe('snapshots', () => {
test('basic snapshot', () => {
const el = shallow(<SelectGroup {...props} />);
expect(el.snapshot).toMatchSnapshot();
});
test('disabled', () => {
const el = shallow(<SelectGroup {...props} disabled />);
expect(el.snapshot).toMatchSnapshot();
});
});
});
});

View File

@@ -0,0 +1,72 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StudentGroupsFilter component render snapshot 1`] = `
<Fragment>
<SelectGroup
id="Tracks"
label="Tracks"
onChange={[MockFunction]}
options={
[
<option
value="Track-All"
>
Track-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
<option
value="v4"
>
n4
</option>,
]
}
value="test-track"
/>
<SelectGroup
disabled={false}
id="Cohorts"
label="Cohorts"
onChange={[MockFunction]}
options={
[
<option
value="Cohort-All"
>
Cohort-All
</option>,
<option
value="v1"
>
n1
</option>,
<option
value="v2"
>
n2
</option>,
<option
value="v3"
>
n3
</option>,
]
}
value="test-cohort"
/>
</Fragment>
`;

View File

@@ -1,164 +1,84 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import SelectGroup from '../SelectGroup';
import { StudentGroupsFilter } from './index';
import useStudentGroupsFilterData from './hooks';
import StudentGroupsFilter from '.';
jest.mock('../SelectGroup', () => jest.fn(() => <div data-testid="select-group">SelectGroup</div>));
jest.mock('./hooks', () => jest.fn());
jest.mock('../SelectGroup', () => 'SelectGroup');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
initializeMocks();
describe('StudentGroupsFilter', () => {
const mockUpdateQueryParams = jest.fn();
const mockTracksData = {
value: 'test-track-value',
const props = {
cohorts: {
value: 'test-cohort',
entries: [
{ value: 'track1', name: 'Track 1' },
{ value: 'track2', name: 'Track 2' },
],
handleChange: jest.fn(),
};
const mockCohortsData = {
value: 'test-cohort-value',
entries: [
{ value: 'cohort1', name: 'Cohort 1' },
{ value: 'cohort2', name: 'Cohort 2' },
{ value: 'v1', name: 'n1' },
{ value: 'v2', name: 'n2' },
{ value: 'v3', name: 'n3' },
],
handleChange: jest.fn(),
isDisabled: false,
};
},
tracks: {
value: 'test-track',
entries: [
{ value: 'v1', name: 'n1' },
{ value: 'v2', name: 'n2' },
{ value: 'v3', name: 'n3' },
{ value: 'v4', name: 'n4' },
],
handleChange: jest.fn(),
},
};
useStudentGroupsFilterData.mockReturnValue(props);
const updateQueryParams = jest.fn();
beforeEach(() => {
let el;
describe('StudentGroupsFilter component', () => {
beforeAll(() => {
jest.clearAllMocks();
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: mockCohortsData,
el = shallow(<StudentGroupsFilter updateQueryParams={updateQueryParams} />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
it('calls useStudentGroupsFilterData hook with updateQueryParams', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(useStudentGroupsFilterData).toHaveBeenCalledWith({
updateQueryParams: mockUpdateQueryParams,
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
it('renders two SelectGroup components', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
expect(SelectGroup).toHaveBeenCalledTimes(2);
expect(screen.getAllByTestId('select-group')).toHaveLength(2);
});
describe('tracks SelectGroup', () => {
it('renders tracks SelectGroup with correct props', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
expect(tracksCall.id).toBe('Tracks');
expect(tracksCall.value).toBe(mockTracksData.value);
expect(tracksCall.onChange).toBe(mockTracksData.handleChange);
test('track options', () => {
const {
options,
onChange,
value,
} = el.instance.findByType(SelectGroup)[0].props;
expect(value).toEqual(props.tracks.value);
expect(onChange).toEqual(props.tracks.handleChange);
expect(options.length).toEqual(5);
const testEntry = props.tracks.entries[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testEntry.value);
expect(optionProps.children).toEqual(testEntry.name);
});
it('includes trackAll option in tracks SelectGroup', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
const { options } = tracksCall;
expect(options).toHaveLength(3);
expect(options[0].props.value).toBeDefined();
expect(options[0].props.children).toBeDefined();
});
it('includes track entries in tracks SelectGroup options', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
const { options } = tracksCall;
expect(options[1].props.value).toBe('track1');
expect(options[1].props.children).toBe('Track 1');
expect(options[2].props.value).toBe('track2');
expect(options[2].props.children).toBe('Track 2');
});
});
describe('cohorts SelectGroup', () => {
it('renders cohorts SelectGroup with correct props', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.id).toBe('Cohorts');
expect(cohortsCall.value).toBe(mockCohortsData.value);
expect(cohortsCall.onChange).toBe(mockCohortsData.handleChange);
expect(cohortsCall.disabled).toBe(mockCohortsData.isDisabled);
});
it('includes cohortAll option in cohorts SelectGroup', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
const { options } = cohortsCall;
expect(options).toHaveLength(3);
expect(options[0].props.value).toBeDefined();
expect(options[0].props.children).toBeDefined();
});
it('includes cohort entries in cohorts SelectGroup options', () => {
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
const { options } = cohortsCall;
expect(options[1].props.value).toBe('cohort1');
expect(options[1].props.children).toBe('Cohort 1');
expect(options[2].props.value).toBe('cohort2');
expect(options[2].props.children).toBe('Cohort 2');
});
it('passes disabled state to cohorts SelectGroup', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: { ...mockCohortsData, isDisabled: true },
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.disabled).toBe(true);
});
});
describe('with empty entries', () => {
it('handles empty tracks entries', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: { ...mockTracksData, entries: [] },
cohorts: mockCohortsData,
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const tracksCall = SelectGroup.mock.calls[0][0];
expect(tracksCall.options).toHaveLength(1);
});
it('handles empty cohorts entries', () => {
useStudentGroupsFilterData.mockReturnValue({
tracks: mockTracksData,
cohorts: { ...mockCohortsData, entries: [] },
});
render(<StudentGroupsFilter updateQueryParams={mockUpdateQueryParams} />);
const cohortsCall = SelectGroup.mock.calls[1][0];
expect(cohortsCall.options).toHaveLength(1);
test('cohort options', () => {
const {
options,
onChange,
disabled,
value,
} = el.instance.findByType(SelectGroup)[1].props;
expect(value).toEqual(props.cohorts.value);
expect(disabled).toEqual(false);
expect(onChange).toEqual(props.cohorts.handleChange);
expect(options.length).toEqual(4);
const testEntry = props.cohorts.entries[0];
const optionProps = options[1].props;
expect(optionProps.value).toEqual(testEntry.value);
expect(optionProps.children).toEqual(testEntry.name);
});
});
});

View File

@@ -0,0 +1,57 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PercentGroup Component snapshots basic snapshot 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={false}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;
exports[`PercentGroup Component snapshots disabled 1`] = `
<div
className="percent-group"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
disabled={true}
max={100}
min={0}
onChange={[MockFunction props.onChange]}
step={1}
type="number"
value="group VALUE"
/>
</Form.Group>
<span
className="input-percent-label"
>
%
</span>
</div>
`;

View File

@@ -0,0 +1,79 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SelectGroup Component snapshots basic snapshot 1`] = `
<div
className="student-filters"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
as="select"
disabled={false}
onChange={[MockFunction props.onChange]}
value="group VALUE"
>
<option
key="opt1"
value="opt1"
>
Option 1
</option>
<option
key="opt2"
value="opt2"
>
Option 2
</option>
<option
key="opt3"
value="opt3"
>
Option 3
</option>
</Form.Control>
</Form.Group>
</div>
`;
exports[`SelectGroup Component snapshots disabled 1`] = `
<div
className="student-filters"
>
<Form.Group
controlId="group id"
>
<Form.Label>
Group Label
</Form.Label>
<Form.Control
as="select"
disabled={true}
onChange={[MockFunction props.onChange]}
value="group VALUE"
>
<option
key="opt1"
value="opt1"
>
Option 1
</option>
<option
key="opt2"
value="opt2"
>
Option 2
</option>
<option
key="opt3"
value="opt3"
>
Option 3
</option>
</Form.Control>
</Form.Group>
</div>
`;

View File

@@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookFilters render snapshot 1`] = `
<Fragment>
<div
className="filter-sidebar-header"
>
<h2>
<Icon
className="fa fa-filter"
/>
</h2>
<IconButton
alt="Close Filters"
aria-label="Close Filters"
className="p-1"
iconAs="Icon"
onClick={[MockFunction hook.closeMenu]}
src="Close"
/>
</div>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Assignments"
>
<div>
<AssignmentTypeFilter
updateQueryParams={[MockFunction]}
/>
<AssignmentFilter
updateQueryParams={[MockFunction]}
/>
<AssignmentGradeFilter
updateQueryParams={[MockFunction]}
/>
</div>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Overall Grade"
>
<CourseGradeFilter
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Student Groups"
>
<StudentGroupsFilter
updateQueryParams={[MockFunction]}
/>
</Collapsible>
<Collapsible
className="filter-group mb-3"
defaultOpen={true}
title="Include Course Team Members"
>
<Form.Checkbox
checked={true}
onChange={[MockFunction hook.handleChange]}
>
Include Course Team Members
</Form.Checkbox>
</Collapsible>
</Fragment>
`;

View File

@@ -1,30 +1,82 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Collapsible } from '@openedx/paragon';
import { formatMessage } from 'testUtils';
import AssignmentTypeFilter from './AssignmentTypeFilter';
import AssignmentFilter from './AssignmentFilter';
import AssignmentGradeFilter from './AssignmentGradeFilter';
import CourseGradeFilter from './CourseGradeFilter';
import StudentGroupsFilter from './StudentGroupsFilter';
import messages from './messages';
import useGradebookFiltersData from './hooks';
import GradebookFilters from '.';
const updateQueryParams = jest.fn();
jest.mock('./AssignmentTypeFilter', () => 'AssignmentTypeFilter');
jest.mock('./AssignmentFilter', () => 'AssignmentFilter');
jest.mock('./AssignmentGradeFilter', () => 'AssignmentGradeFilter');
jest.mock('./CourseGradeFilter', () => 'CourseGradeFilter');
jest.mock('./StudentGroupsFilter', () => 'StudentGroupsFilter');
initializeMocks();
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const hookProps = {
closeMenu: jest.fn().mockName('hook.closeMenu'),
includeCourseTeamMembers: {
value: true,
handleChange: jest.fn().mockName('hook.handleChange'),
},
};
useGradebookFiltersData.mockReturnValue(hookProps);
let el;
const updateQueryParams = jest.fn();
describe('GradebookFilters', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<GradebookFilters updateQueryParams={updateQueryParams} />);
el = shallow(<GradebookFilters updateQueryParams={updateQueryParams} />);
});
describe('All filters render together', () => {
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookFiltersData).toHaveBeenCalledWith({ updateQueryParams });
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Assignment filters', () => {
expect(screen.getByRole('combobox', { name: 'Assignment Types' })).toBeInTheDocument();
expect(screen.getByRole('combobox', { name: 'Assignment' })).toBeInTheDocument();
expect(el.instance.findByType(Collapsible)[0].children[0]).toMatchObject(shallow(
<div>
<AssignmentTypeFilter updateQueryParams={updateQueryParams} />
<AssignmentFilter updateQueryParams={updateQueryParams} />
<AssignmentGradeFilter updateQueryParams={updateQueryParams} />
</div>,
));
});
test('CourseGrade filters', () => {
expect(screen.getByRole('button', { name: 'Overall Grade' })).toBeInTheDocument();
expect(el.instance.findByType(Collapsible)[1].children[0]).toMatchObject(shallow(
<CourseGradeFilter updateQueryParams={updateQueryParams} />,
));
});
test('StudentGroups filters', () => {
expect(screen.getByRole('button', { name: 'Student Groups' })).toBeInTheDocument();
expect(el.instance.findByType(Collapsible)[2].children[0]).toMatchObject(shallow(
<StudentGroupsFilter updateQueryParams={updateQueryParams} />,
));
});
test('includeCourseTeamMembers', () => {
expect(screen.getByRole('button', { name: 'Include Course Team Members' })).toBeInTheDocument();
const checkbox = el.instance.findByType(Collapsible)[3].children[0];
expect(checkbox.props).toEqual({
checked: true,
onChange: hookProps.includeCourseTeamMembers.handleChange,
});
expect(checkbox.children[0].el).toEqual(formatMessage(messages.includeCourseTeamMembers));
});
});
});

View File

@@ -0,0 +1,139 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookHeader component render default view shapshot 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
</div>
`;
exports[`GradebookHeader component render frozen grades snapshot: show frozen warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
The grades for this course are now frozen. Editing of grades is no longer allowed.
</div>
</div>
`;
exports[`GradebookHeader component render show bulk management snapshot: show toggle view message button with handleToggleViewClick method 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
<Button
onClick={[MockFunction hooks.handleToggleViewClick]}
variant="tertiary"
>
toggle-view-message
</Button>
</div>
</div>
`;
exports[`GradebookHeader component render user cannot view gradebook snapshot: show unauthorized warning 1`] = `
<div
className="gradebook-header"
>
<a
className="mb-3"
href="test-dashboard-url"
>
<span
aria-hidden="true"
>
&lt;&lt;
</span>
Back to Dashboard
</a>
<h1>
Gradebook
</h1>
<div
className="subtitle-row d-flex justify-content-between align-items-center"
>
<h2
className="text-break"
>
test-course-id
</h2>
</div>
<div
className="alert alert-warning"
role="alert"
>
You are not authorized to view the gradebook for this course.
</div>
</div>
`;

View File

@@ -1,300 +1,77 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon';
import { formatMessage } from 'testUtils';
import { instructorDashboardUrl } from 'data/services/lms/urls';
import { GradebookHeader } from './index';
import useGradebookHeaderData from './hooks';
import messages from './messages';
import GradebookHeader from '.';
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
jest.mock('data/services/lms/urls', () => ({
instructorDashboardUrl: jest.fn(),
}));
jest.mock('./hooks', () => jest.fn());
initializeMocks();
instructorDashboardUrl.mockReturnValue('test-dashboard-url');
describe('GradebookHeader', () => {
const mockHandleToggleViewClick = jest.fn();
const hookProps = {
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'test-course-id',
handleToggleViewClick: jest.fn().mockName('hooks.handleToggleViewClick'),
showBulkManagement: false,
toggleViewMessage: { defaultMessage: 'toggle-view-message' },
};
useGradebookHeaderData.mockReturnValue(hookProps);
beforeEach(() => {
jest.clearAllMocks();
instructorDashboardUrl.mockReturnValue('https://example.com/dashboard');
let el;
describe('GradebookHeader component', () => {
beforeAll(() => {
el = shallow(<GradebookHeader />);
});
describe('basic rendering', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('renders the main header container', () => {
render(<GradebookHeader />);
const header = screen.getByText('Gradebook').closest('.gradebook-header');
expect(header).toHaveClass('gradebook-header');
});
it('renders back to dashboard link', () => {
render(<GradebookHeader />);
const dashboardLink = screen.getByRole('link');
expect(dashboardLink).toHaveAttribute(
'href',
'https://example.com/dashboard',
);
expect(dashboardLink).toHaveClass('mb-3');
expect(dashboardLink).toHaveTextContent('Back to Dashboard');
});
it('renders gradebook title', () => {
render(<GradebookHeader />);
const title = screen.getByRole('heading', { level: 1 });
expect(title).toHaveTextContent('Gradebook');
});
it('renders course ID subtitle', () => {
render(<GradebookHeader />);
const subtitle = screen.getByRole('heading', { level: 2 });
expect(subtitle).toHaveTextContent('course-v1:TestU+CS101+2024');
expect(subtitle).toHaveClass('text-break');
});
it('renders subtitle row with correct classes', () => {
render(<GradebookHeader />);
const subtitleRow = screen.getByRole('heading', {
level: 2,
}).parentElement;
expect(subtitleRow).toHaveClass(
'subtitle-row',
'd-flex',
'justify-content-between',
'align-items-center',
);
});
it('calls instructorDashboardUrl to get dashboard URL', () => {
render(<GradebookHeader />);
expect(instructorDashboardUrl).toHaveBeenCalled();
});
it('calls useGradebookHeaderData hook', () => {
render(<GradebookHeader />);
expect(useGradebookHeaderData).toHaveBeenCalled();
describe('behavior', () => {
it('initializes hooks', () => {
expect(useGradebookHeaderData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('bulk management toggle button', () => {
describe('when showBulkManagement is true', () => {
describe('render', () => {
describe('default view', () => {
test('shapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
describe('show bulk management', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toActivityLog,
});
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, showBulkManagement: true });
el = shallow(<GradebookHeader />);
});
it('renders toggle view button', () => {
render(<GradebookHeader />);
expect(screen.getByRole('button')).toBeInTheDocument();
});
it('displays correct button text from toggleViewMessage', () => {
render(<GradebookHeader />);
const toggleButton = screen.getByRole('button');
expect(toggleButton).toHaveTextContent('View Bulk Management History');
});
it('calls handleToggleViewClick when button is clicked', async () => {
render(<GradebookHeader />);
const user = userEvent.setup();
const toggleButton = screen.getByRole('button');
await user.click(toggleButton);
expect(mockHandleToggleViewClick).toHaveBeenCalledTimes(1);
});
it('displays correct message from toggleViewMessage', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toGradesView,
});
render(<GradebookHeader />);
const toggleButton = screen.getByRole('button');
expect(toggleButton).toHaveTextContent('Return to Gradebook');
test('snapshot: show toggle view message button with handleToggleViewClick method', () => {
expect(el.snapshot).toMatchSnapshot();
const { onClick } = el.instance.findByType(Button)[0].props;
expect(onClick).toEqual(hookProps.handleToggleViewClick);
expect(el.instance.findByType(Button)[0].children[0].el).toEqual(formatMessage(hookProps.toggleViewMessage));
});
});
describe('when showBulkManagement is false', () => {
describe('frozen grades', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, areGradesFrozen: true });
el = shallow(<GradebookHeader />);
});
it('does not render toggle view button', () => {
render(<GradebookHeader />);
expect(screen.queryByRole('button')).not.toBeInTheDocument();
test('snapshot: show frozen warning', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
});
describe('frozen grades warning', () => {
describe('when areGradesFrozen is true', () => {
describe('user cannot view gradebook', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: true,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
useGradebookHeaderData.mockReturnValueOnce({ ...hookProps, canUserViewGradebook: false });
el = shallow(<GradebookHeader />);
});
it('renders frozen warning alert', () => {
render(<GradebookHeader />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert', 'alert-warning');
expect(alert).toHaveTextContent(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
);
test('snapshot: show unauthorized warning', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
describe('when areGradesFrozen is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('does not render frozen warning alert', () => {
render(<GradebookHeader />);
expect(
screen.queryByText(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
),
).not.toBeInTheDocument();
});
});
});
describe('unauthorized warning', () => {
describe('when canUserViewGradebook is false', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: false,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('renders unauthorized warning alert', () => {
render(<GradebookHeader />);
const alert = screen.getByRole('alert');
expect(alert).toHaveClass('alert', 'alert-warning');
expect(alert).toHaveTextContent(
'You are not authorized to view the gradebook for this course.',
);
});
});
describe('when canUserViewGradebook is true', () => {
beforeEach(() => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
});
it('does not render unauthorized warning alert', () => {
render(<GradebookHeader />);
expect(
screen.queryByText(
'You are not authorized to view the gradebook for this course.',
),
).not.toBeInTheDocument();
});
});
});
describe('multiple warnings', () => {
it('renders both frozen and unauthorized warnings when both conditions are true', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: true,
canUserViewGradebook: false,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: false,
toggleViewMessage: messages.toActivityLog,
});
render(<GradebookHeader />);
const alerts = screen.getAllByRole('alert');
expect(alerts).toHaveLength(2);
expect(
screen.getByText(
'The grades for this course are now frozen. Editing of grades is no longer allowed.',
),
).toBeInTheDocument();
expect(
screen.getByText(
'You are not authorized to view the gradebook for this course.',
),
).toBeInTheDocument();
});
});
describe('complete integration', () => {
it('renders all elements when showBulkManagement is true', () => {
useGradebookHeaderData.mockReturnValue({
areGradesFrozen: false,
canUserViewGradebook: true,
courseId: 'course-v1:TestU+CS101+2024',
handleToggleViewClick: mockHandleToggleViewClick,
showBulkManagement: true,
toggleViewMessage: messages.toActivityLog,
});
render(<GradebookHeader />);
expect(screen.getByRole('link')).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 1 })).toBeInTheDocument();
expect(screen.getByRole('heading', { level: 2 })).toBeInTheDocument();
expect(screen.getByRole('button')).toBeInTheDocument();
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
});
});

View File

@@ -0,0 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`BulkManagementControls render snapshot - show - network and import buttons 1`] = `
<div
className="d-flex"
>
<NetworkButton
label={
{
"defaultMessage": "Download Grades",
"description": "A labeled button that allows an admin user to download course grades all at once (in bulk).",
"id": "gradebook.GradesView.BulkManagementControls.bulkManagementLabel",
}
}
onClick={[MockFunction]}
/>
<ImportGradesButton />
</div>
`;

View File

@@ -1,160 +1,32 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import NetworkButton from 'components/NetworkButton';
import ImportGradesButton from '../ImportGradesButton';
import { BulkManagementControls } from './index';
import useBulkManagementControlsData from './hooks';
import messages from './messages';
import BulkManagementControls from '.';
jest.mock('../ImportGradesButton', () => 'ImportGradesButton');
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('components/NetworkButton', () => jest.fn(() => <div data-testid="network-button">NetworkButton</div>));
jest.mock('../ImportGradesButton', () => jest.fn(() => (
<div data-testid="import-grades-button">ImportGradesButton</div>
)));
jest.mock('./hooks', () => jest.fn());
initializeMocks();
const hookProps = {
show: true,
handleClickExportGrades: jest.fn(),
};
useBulkManagementControlsData.mockReturnValue(hookProps);
describe('BulkManagementControls', () => {
const mockHandleClickExportGrades = jest.fn();
beforeEach(() => {
jest.clearAllMocks();
describe('behavior', () => {
shallow(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledWith();
});
describe('when show is false', () => {
beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
describe('render', () => {
test('snapshot - show - network and import buttons', () => {
expect(shallow(<BulkManagementControls />).snapshot).toMatchSnapshot();
});
it('renders nothing when show is false', () => {
render(<BulkManagementControls />);
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
expect(
screen.queryByTestId('import-grades-button'),
).not.toBeInTheDocument();
});
it('does not render NetworkButton when show is false', () => {
render(<BulkManagementControls />);
expect(NetworkButton).not.toHaveBeenCalled();
});
it('does not render ImportGradesButton when show is false', () => {
render(<BulkManagementControls />);
expect(ImportGradesButton).not.toHaveBeenCalled();
});
});
describe('when show is true', () => {
beforeEach(() => {
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
});
it('renders the container div with correct class when show is true', () => {
render(<BulkManagementControls />);
const containerDiv = screen.getByTestId('network-button').parentElement;
expect(containerDiv).toHaveClass('d-flex');
});
it('renders NetworkButton with correct props', () => {
render(<BulkManagementControls />);
expect(NetworkButton).toHaveBeenCalledWith(
{
label: messages.downloadGradesBtn,
onClick: mockHandleClickExportGrades,
},
{},
);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('renders ImportGradesButton', () => {
render(<BulkManagementControls />);
expect(ImportGradesButton).toHaveBeenCalledWith({}, {});
expect(screen.getByTestId('import-grades-button')).toBeInTheDocument();
});
it('calls handleClickExportGrades when NetworkButton is clicked', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
const { onClick } = networkButtonCall;
onClick();
expect(mockHandleClickExportGrades).toHaveBeenCalledTimes(1);
});
it('passes correct label to NetworkButton', () => {
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.label).toBe(messages.downloadGradesBtn);
});
it('renders both buttons in the correct order', () => {
render(<BulkManagementControls />);
expect(NetworkButton).toHaveBeenCalled();
expect(ImportGradesButton).toHaveBeenCalled();
const networkButton = screen.getByTestId('network-button');
const importButton = screen.getByTestId('import-grades-button');
expect(networkButton).toBeInTheDocument();
expect(importButton).toBeInTheDocument();
});
});
describe('hook integration', () => {
it('calls useBulkManagementControlsData hook', () => {
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(useBulkManagementControlsData).toHaveBeenCalledTimes(1);
});
it('uses the show value from hook to determine rendering', () => {
useBulkManagementControlsData.mockReturnValue({
show: false,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.queryByTestId('network-button')).not.toBeInTheDocument();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: mockHandleClickExportGrades,
});
render(<BulkManagementControls />);
expect(screen.getByTestId('network-button')).toBeInTheDocument();
});
it('passes handleClickExportGrades from hook to NetworkButton', () => {
const customHandler = jest.fn();
useBulkManagementControlsData.mockReturnValue({
show: true,
handleClickExportGrades: customHandler,
});
render(<BulkManagementControls />);
const networkButtonCall = NetworkButton.mock.calls[0][0];
expect(networkButtonCall.onClick).toBe(customHandler);
test('snapshot - empty if show is not truthy', () => {
useBulkManagementControlsData.mockReturnValueOnce({ ...hookProps, show: false });
expect(shallow(<BulkManagementControls />).isEmptyRender()).toEqual(true);
});
});
});

View File

@@ -1,100 +1,17 @@
import React from 'react';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import HistoryHeader from './HistoryHeader';
initializeMocks();
describe('HistoryHeader', () => {
const defaultProps = {
id: 'test-id',
label: 'Test Label',
value: 'Test Value',
const props = {
id: 'water',
label: 'Brita',
value: 'hydration',
};
it('renders header with label and value', () => {
render(<HistoryHeader {...defaultProps} />);
expect(screen.getByText('Test Label:')).toBeInTheDocument();
expect(screen.getByText('Test Value')).toBeInTheDocument();
});
it('renders header element with correct classes', () => {
render(<HistoryHeader {...defaultProps} />);
const headerElement = screen.getByText('Test Label:');
expect(headerElement).toHaveClass('grade-history-header');
expect(headerElement).toHaveClass('grade-history-test-id');
});
it('renders with string value', () => {
const props = {
...defaultProps,
value: 'String Value',
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('String Value')).toBeInTheDocument();
});
it('renders with number value', () => {
const props = {
...defaultProps,
value: 85,
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('85')).toBeInTheDocument();
});
it('renders with null value (default prop)', () => {
const props = {
id: 'test-id',
label: 'Test Label',
};
render(<HistoryHeader {...props} />);
expect(screen.getByText('Test Label:')).toBeInTheDocument();
const valueDiv = screen.getByText('Test Label:').nextSibling;
expect(valueDiv).toBeInTheDocument();
expect(valueDiv).toBeEmptyDOMElement();
});
it('renders with React node as label', () => {
const props = {
...defaultProps,
label: <strong>Bold Label</strong>,
};
render(<HistoryHeader {...props} />);
const strongElement = screen.getByText('Bold Label');
expect(strongElement.tagName).toBe('STRONG');
});
it('generates correct class name based on id', () => {
const props = {
...defaultProps,
id: 'assignment-name',
};
render(<HistoryHeader {...props} />);
const headerElement = screen.getByText('Test Label:');
expect(headerElement).toHaveClass('grade-history-assignment-name');
});
it('renders container structure correctly', () => {
render(<HistoryHeader {...defaultProps} />);
const headerElement = screen.getByText('Test Label:');
const valueElement = screen.getByText('Test Value');
expect(headerElement).toBeInTheDocument();
expect(valueElement).toBeInTheDocument();
expect(headerElement).toHaveClass(
'grade-history-header',
'grade-history-test-id',
);
describe('Component', () => {
test('snapshot', () => {
expect(shallow(<HistoryHeader {...props} />).snapshot).toMatchSnapshot();
});
});
});

View File

@@ -1,8 +1,16 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { selectors } from 'data/redux/hooks';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import ModalHeaders from './ModalHeaders';
import { formatMessage } from 'testUtils';
import HistoryHeader from './HistoryHeader';
import ModalHeaders, { HistoryKeys } from './ModalHeaders';
import messages from './messages';
jest.mock('./HistoryHeader', () => 'HistoryHeader');
jest.mock('data/redux/hooks', () => ({
selectors: {
@@ -21,25 +29,57 @@ const gradeData = {
gradeOriginalEarnedGraded: 'test-original-grade',
};
selectors.grades.useGradeData.mockReturnValue(gradeData);
initializeMocks();
let el;
describe('ModalHeaders', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<ModalHeaders />);
el = shallow(<ModalHeaders />);
});
describe('behavior', () => {
it('initializes intl', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.app.useModalData).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('assignment header', () => {
expect(screen.getByText(modalData.assignmentName)).toBeInTheDocument();
const headerProps = el.instance.findByType(HistoryHeader)[0].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.assignment,
label: formatMessage(messages.assignmentHeader),
value: modalData.assignmentName,
});
});
test('student header', () => {
expect(screen.getByText(modalData.updateUserName)).toBeInTheDocument();
const headerProps = el.instance.findByType(HistoryHeader)[1].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.student,
label: formatMessage(messages.studentHeader),
value: modalData.updateUserName,
});
});
test('originalGrade header', () => {
expect(screen.getByText(gradeData.gradeOriginalEarnedGraded)).toBeInTheDocument();
const headerProps = el.instance.findByType(HistoryHeader)[2].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.originalGrade,
label: formatMessage(messages.originalGradeHeader),
value: gradeData.gradeOriginalEarnedGraded,
});
});
test('currentGrade header', () => {
expect(screen.getByText(gradeData.gradeOverrideCurrentEarnedGradedOverride)).toBeInTheDocument();
const headerProps = el.instance.findByType(HistoryHeader)[3].props;
expect(headerProps).toMatchObject({
id: HistoryKeys.currentGrade,
label: formatMessage(messages.currentGradeHeader),
value: gradeData.gradeOverrideCurrentEarnedGradedOverride,
});
});
});
});

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`AdjustedGradeInput component render snapshot 1`] = `
<span>
<Form.Control
name="adjustedGradeValue"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
some-hint-text
</span>
`;

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/paragon';
import useAdjustedGradeInputData from './hooks';
import AdjustedGradeInput from '.';
@@ -13,17 +15,24 @@ const hookProps = {
};
useAdjustedGradeInputData.mockReturnValue(hookProps);
let el;
describe('AdjustedGradeInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<AdjustedGradeInput />);
el = shallow(<AdjustedGradeInput />);
});
describe('behavior', () => {
it('initializes hook data', () => {
expect(useAdjustedGradeInputData).toHaveBeenCalled();
});
});
describe('render', () => {
test('renders input with correct props', () => {
const input = screen.getByRole('textbox');
expect(input).toBeInTheDocument();
expect(input).toHaveValue(hookProps.value);
expect(screen.getByText(hookProps.hintText)).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const control = el.instance.findByType(Form.Control)[0];
expect(control.props.value).toEqual(hookProps.value);
expect(control.props.onChange).toEqual(hookProps.onChange);
expect(el.instance.children[1].el).toContain(hookProps.hintText);
});
});
});

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ReasonInput component render snapshot 1`] = `
<Form.Control
data-testid="reason-input-control"
name="reasonForChange"
onChange={[MockFunction hook.onChange]}
type="text"
value="test-value"
/>
`;

View File

@@ -16,12 +16,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
}));
const modalData = { reasonForChange: 'test-reason-for-change' };
const setModalState = jest.fn();
selectors.app.useModalData.mockReturnValue(modalData);
@@ -31,7 +25,6 @@ const ref = { current: { focus: jest.fn() }, useRef: true };
React.useRef.mockReturnValue(ref);
let out;
describe('useReasonInputData hook', () => {
beforeEach(() => {
jest.clearAllMocks();

View File

@@ -1,4 +1,7 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { Form } from '@openedx/paragon';
import useReasonInputData from './hooks';
import ReasonInput from '.';
@@ -6,26 +9,29 @@ import ReasonInput from '.';
jest.mock('./hooks', () => jest.fn());
const hookProps = {
ref: jest.fn().mockName('hook.ref'),
ref: 'reason-input-ref',
onChange: jest.fn().mockName('hook.onChange'),
value: 'test-value',
};
useReasonInputData.mockReturnValue(hookProps);
let el;
describe('ReasonInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<ReasonInput />);
el = shallow(<ReasonInput />);
});
describe('behavior', () => {
it('initializes hook data', () => {
expect(useReasonInputData).toHaveBeenCalled();
});
});
describe('renders', () => {
it('input correctly', () => {
expect(screen.getByRole('textbox')).toBeInTheDocument();
expect(screen.getByRole('textbox')).toHaveValue(hookProps.value);
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const control = el.instance.findByType(Form.Control)[0];
expect(control.props.value).toEqual(hookProps.value);
expect(control.props.onChange).toEqual(hookProps.onChange);
});
});
});

View File

@@ -4,6 +4,8 @@ import { render } from '@testing-library/react';
import useReasonInputData from './hooks';
import ReasonInput, { controlTestId } from '.';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
const focus = jest.fn();

View File

@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`OverrideTable component render snapshot 1`] = `
<DataTable
columns="test-columns"
data={
[
{
"test": "data",
},
{
"andOther": "test-data",
},
{
"adjustedGrade": <AdjustedGradeInput />,
"date": {
"formatted": 2000-01-01T00:00:00.000Z,
},
"reason": <ReasonInput />,
},
]
}
itemCount={3}
/>
`;

View File

@@ -1,4 +1,5 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { gradeOverrideHistoryColumns as columns } from 'data/constants/app';
import { selectors } from 'data/redux/hooks';
@@ -15,18 +16,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
selectors.grades.useHasOverrideErrors.mockReturnValue(false);
const gradeOverrides = ['some', 'override', 'data'];
const gradeData = { gradeOverrideHistoryResults: gradeOverrides };
@@ -55,22 +44,22 @@ describe('useOverrideTableData', () => {
describe('columns', () => {
test('date column', () => {
const { Header, accessor } = out.columns[0];
expect(Header).toEqual(messages.dateHeader.defaultMessage);
expect(Header).toEqual(formatMessage(messages.dateHeader));
expect(accessor).toEqual(columns.date);
});
test('grader column', () => {
const { Header, accessor } = out.columns[1];
expect(Header).toEqual(messages.graderHeader.defaultMessage);
expect(Header).toEqual(formatMessage(messages.graderHeader));
expect(accessor).toEqual(columns.grader);
});
test('reason column', () => {
const { Header, accessor } = out.columns[2];
expect(Header).toEqual(messages.reasonHeader.defaultMessage);
expect(Header).toEqual(formatMessage(messages.reasonHeader));
expect(accessor).toEqual(columns.reason);
});
test('adjustedGrade column', () => {
const { Header, accessor } = out.columns[3];
expect(Header).toEqual(messages.adjustedGradeHeader.defaultMessage);
expect(Header).toEqual(formatMessage(messages.adjustedGradeHeader));
expect(accessor).toEqual(columns.adjustedGrade);
});
});

View File

@@ -1,61 +1,65 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/paragon';
import { formatDateForDisplay } from 'utils';
import AdjustedGradeInput from './AdjustedGradeInput';
import ReasonInput from './ReasonInput';
import useOverrideTableData from './hooks';
import OverrideTable from '.';
import { renderWithIntl } from '../../../../testUtilsExtra';
jest.mock('utils', () => ({
...jest.requireActual('utils'),
formatDateForDisplay: (date) => ({ formatted: date }),
}));
jest.mock('./hooks', () => jest.fn());
jest.mock('./AdjustedGradeInput', () => 'AdjustedGradeInput');
jest.mock('./ReasonInput', () => 'ReasonInput');
const hookProps = {
hide: false,
data: [
{ filename: 'data' },
{ resultsSummary: 'test-data' },
{ test: 'data' },
{ andOther: 'test-data' },
],
columns: [{
Header: 'Gradebook',
accessor: 'filename',
},
{
Header: 'Download Summary',
accessor: 'resultsSummary',
}],
columns: 'test-columns',
};
useOverrideTableData.mockReturnValue(hookProps);
let el;
describe('OverrideTable component', () => {
beforeEach(() => {
jest
.clearAllMocks()
.useFakeTimers('modern')
.setSystemTime(new Date('2000-01-01').getTime());
el = shallow(<OverrideTable />);
});
describe('hooks', () => {
describe('behavior', () => {
it('initializes hook data', () => {
useOverrideTableData.mockReturnValue(hookProps);
renderWithIntl(<OverrideTable />);
expect(useOverrideTableData).toHaveBeenCalled();
});
});
describe('behavior', () => {
it('null render if hide', () => {
useOverrideTableData.mockReturnValue({ ...hookProps, hide: true });
renderWithIntl(<OverrideTable />);
expect(screen.queryByRole('table')).toBeNull();
describe('render', () => {
test('null render if hide', () => {
useOverrideTableData.mockReturnValueOnce({ ...hookProps, hide: true });
el = shallow(<OverrideTable />);
expect(el.isEmptyRender()).toEqual(true);
});
it('renders table with correct data', () => {
useOverrideTableData.mockReturnValue(hookProps);
renderWithIntl(<OverrideTable />);
const table = screen.getByRole('table');
expect(table).toBeInTheDocument();
expect(screen.getByText(hookProps.columns[0].Header)).toBeInTheDocument();
expect(screen.getByText(hookProps.columns[1].Header)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[0].filename)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[1].resultsSummary)).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const table = el.instance.findByType(DataTable)[0];
expect(table.props.columns).toEqual(hookProps.columns);
const data = [...table.props.data];
const inputRow = data.pop();
const formattedDate = formatDateForDisplay(new Date());
expect(data).toEqual(hookProps.data);
expect(inputRow).toMatchObject({
adjustedGrade: <AdjustedGradeInput />,
date: formattedDate,
reason: <ReasonInput />,
});
});
});
});

View File

@@ -0,0 +1,15 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`HistoryHeader Component snapshot 1`] = `
<div>
<div
className="grade-history-header grade-history-water"
>
Brita
:
</div>
<div>
hydration
</div>
</div>
`;

View File

@@ -0,0 +1,26 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ModalHeaders render snapshot 1`] = `
<div>
<HistoryHeader
id="assignment"
label="Assignment"
value="test-assignment-name"
/>
<HistoryHeader
id="student"
label="Student"
value="test-user-name"
/>
<HistoryHeader
id="original-grade"
label="Original Grade"
value="test-original-grade"
/>
<HistoryHeader
id="current-grade"
label="Current Grade"
value="test-current-grade"
/>
</div>
`;

View File

@@ -0,0 +1,91 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`EditModal component render with error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={true}
variant="danger"
>
test-error
</Alert>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;
exports[`EditModal component render without error snapshot 1`] = `
<ModalDialog
hasCloseButton={true}
isFullscreenOnMobile={true}
isOpen="test-is-open"
onClose={[MockFunction hooks.onClose]}
size="xl"
title="Edit Grades"
>
<ModalDialog.Body>
<div>
<ModalHeaders />
<Alert
dismissible={false}
show={false}
variant="danger"
/>
<OverrideTable />
<div>
Showing most recent actions (max 5). To see more, please contact support
</div>
<div>
Note: Once you save, your changes will be visible to students.
</div>
</div>
</ModalDialog.Body>
<ModalDialog.Footer>
<ActionRow>
<ModalDialog.CloseButton
variant="tertiary"
>
Cancel
</ModalDialog.CloseButton>
<Button
onClick={[MockFunction hooks.handleAdjustedGradeClick]}
variant="primary"
>
Save Grades
</Button>
</ActionRow>
</ModalDialog.Footer>
</ModalDialog>
`;

View File

@@ -1,102 +1,126 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import {
ActionRow,
ModalDialog,
} from '@openedx/paragon';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import ModalHeaders from './ModalHeaders';
import OverrideTable from './OverrideTable';
import useEditModalData from './hooks';
import EditModal from '.';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('./hooks', () => jest.fn());
jest.mock('./ModalHeaders', () => jest.fn(() => <div>ModalHeaders</div>));
jest.mock('./OverrideTable', () => jest.fn(() => <div>OverrideTable</div>));
jest.mock('./ModalHeaders', () => 'ModalHeaders');
jest.mock('./OverrideTable', () => 'OverrideTable');
const hookProps = {
onClose: jest.fn().mockName('hooks.onClose'),
error: 'test-error',
handleAdjustedGradeClick: jest.fn().mockName('hooks.handleAdjustedGradeClick'),
isOpen: true,
isOpen: 'test-is-open',
};
useEditModalData.mockReturnValue(hookProps);
let el;
describe('EditModal component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<EditModal />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes component hooks', () => {
useEditModalData.mockReturnValue(hookProps);
renderWithIntl(<EditModal />);
expect(useEditModalData).toHaveBeenCalled();
});
});
describe('renders', () => {
const testModal = () => {
it('modal properly', () => {
const modal = screen.getByRole('dialog', { title: messages.title.defaultMessage });
expect(modal).toBeInTheDocument();
});
it('triggers onClose when closed', async () => {
const user = userEvent.setup();
const closeButton = screen.getByRole('button', { name: messages.closeText.defaultMessage });
await user.click(closeButton);
expect(hookProps.onClose).toHaveBeenCalled();
});
describe('render', () => {
test('modal props', () => {
const modalProps = el.instance.findByType(ModalDialog)[0].props;
expect(modalProps.title).toEqual(formatMessage(messages.title));
expect(modalProps.isOpen).toEqual(hookProps.isOpen);
expect(modalProps.onClose).toEqual(hookProps.onClose);
});
const loadBody = () => {
const body = el.instance.findByType(ModalDialog)[0].children[0];
const { children } = body.children[0];
return { body, children };
};
const testBody = () => {
it('headers row', () => {
const headers = screen.getByText('ModalHeaders');
expect(headers).toBeInTheDocument();
test('type', () => {
const { body } = loadBody();
expect(body.type).toEqual('ModalDialog.Body');
});
it('table row', () => {
const table = screen.getByText('OverrideTable');
expect(table).toBeInTheDocument();
test('headers row', () => {
const { children } = loadBody();
expect(children[0]).toMatchObject(shallow(<ModalHeaders />));
});
it('messages', () => {
const visibilityMessage = screen.getByText(messages.visibility.defaultMessage);
const saveVisibilityMessage = screen.getByText(messages.saveVisibility.defaultMessage);
expect(visibilityMessage).toBeInTheDocument();
expect(saveVisibilityMessage).toBeInTheDocument();
test('table row', () => {
const { children } = loadBody();
expect(children[2]).toMatchObject(shallow(<OverrideTable />));
});
test('messages', () => {
const { children } = loadBody();
expect(children[3].children[0].el).toEqual(formatMessage(messages.visibility));
expect(children[4].children[0].el).toEqual(formatMessage(messages.saveVisibility));
});
};
const testFooter = () => {
it('adjusted grade button', async () => {
const user = userEvent.setup();
const saveGradeButton = screen.getByRole('button', { name: messages.saveGrade.defaultMessage });
expect(saveGradeButton).toBeInTheDocument();
await user.click(saveGradeButton);
expect(hookProps.handleAdjustedGradeClick).toHaveBeenCalled();
let footer;
beforeEach(() => {
footer = el.instance.findByType(ModalDialog)[0].children;
});
it('close button', async () => {
const user = userEvent.setup();
const cancelButton = screen.getByRole('button', { name: messages.closeText.defaultMessage });
expect(cancelButton).toBeInTheDocument();
await user.click(cancelButton);
expect(hookProps.onClose).toHaveBeenCalled();
test('type', () => {
expect(footer[1].type).toEqual('ModalDialog.Footer');
});
test('contains action row', () => {
expect(footer[1].children[0].type).toEqual('ActionRow');
});
test('close button', () => {
const button = footer[1].findByType(ActionRow)[0].children[0];
expect(button.children[0].el).toEqual(formatMessage(messages.closeText));
expect(button.type).toEqual('ModalDialog.CloseButton');
});
test('adjusted grade button', () => {
const button = footer[1].findByType(ActionRow)[0].children[1];
expect(button.children[0].el).toEqual(formatMessage(messages.saveGrade));
expect(button.type).toEqual('Button');
expect(button.props.onClick).toEqual(hookProps.handleAdjustedGradeClick);
});
};
describe('without error', () => {
beforeEach(() => {
useEditModalData.mockReturnValueOnce({ ...hookProps, error: undefined });
renderWithIntl(<EditModal />);
el = shallow(<EditModal />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testModal();
testBody();
testFooter();
test('alert row', () => {
const alert = screen.queryByRole('alert');
expect(alert).toBeNull();
const alert = loadBody().children[1];
expect(alert.type).toEqual('Alert');
expect(alert.props.show).toEqual(false);
});
});
describe('with error', () => {
beforeEach(() => {
useEditModalData.mockReturnValue(hookProps);
renderWithIntl(<EditModal />);
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
testModal();
testBody();
test('alert row', () => {
const alert = screen.getByRole('alert');
expect(alert).toBeInTheDocument();
expect(alert).toHaveTextContent(hookProps.error);
const alert = loadBody().children[1];
expect(alert.type).toEqual('Alert');
expect(alert.props.show).toEqual(true);
expect(alert.children[0].el).toEqual(hookProps.error);
});
testFooter();
});

View File

@@ -1,12 +1,15 @@
import React from 'react';
import { screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { Button } from '@openedx/paragon';
import { selectors } from 'data/redux/hooks';
import userEvent from '@testing-library/user-event';
import FilterBadge from './FilterBadge';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('@openedx/paragon', () => ({
Button: () => 'Button',
}));
jest.mock('data/redux/hooks', () => ({
selectors: {
root: {
@@ -15,12 +18,11 @@ jest.mock('data/redux/hooks', () => ({
},
}));
const handleClose = jest.fn();
const handleClose = jest.fn(filters => ({ handleClose: filters }));
const filterName = 'test-filter-name';
const hookProps = {
displayName: {
id: 'test.id',
defaultMessage: 'a common name',
},
isDefault: false,
@@ -30,52 +32,63 @@ const hookProps = {
};
selectors.root.useFilterBadgeConfig.mockReturnValue(hookProps);
let el;
describe('FilterBadge', () => {
describe('hooks', () => {
beforeEach(() => {
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
beforeEach(() => {
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.root.useFilterBadgeConfig).toHaveBeenCalledWith(filterName);
});
});
describe('render', () => {
it('empty render if isDefault', () => {
const testDisplayName = () => {
test('formatted display name appears on badge', () => {
expect(el.instance.findByTestId('display-name')[0].children[0].el).toEqual(formatMessage(hookProps.displayName));
});
};
const testCloseButton = () => {
test('close button forwards close method', () => {
expect(el.instance.findByType(Button)[0].props.onClick).toEqual(handleClose(hookProps.connectedFilters));
});
};
test('empty render if isDefault', () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
isDefault: true,
});
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
expect(screen.queryByText(hookProps.displayName)).toBeNull();
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
expect(el.isEmptyRender()).toEqual(true);
});
describe('hide Value', () => {
it('renders display name, value is not shown and close button has correct behavior', async () => {
beforeEach(() => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
hideValue: true,
});
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
const user = userEvent.setup();
expect(screen.getByTestId('display-name')).toHaveTextContent(formatMessage(hookProps.displayName));
expect(screen.queryByTestId('filter-value')).toHaveTextContent('');
const button = screen.getByRole('button', { name: /close/i });
await user.click(button);
expect(handleClose).toHaveBeenCalledWith(hookProps.connectedFilters);
el = shallow(<FilterBadge {...{ handleClose, filterName }} />);
});
testDisplayName();
testCloseButton();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('value is note present in the badge', () => {
expect(el.instance.findByTestId('filter-value')[0].children).toHaveLength(0);
});
});
describe('do not hide value', () => {
it('renders display name and value, and close button has correct behavior', async () => {
selectors.root.useFilterBadgeConfig.mockReturnValueOnce({
...hookProps,
hideValue: false,
});
renderWithIntl(<FilterBadge {...{ handleClose, filterName }} />);
const user = userEvent.setup();
expect(screen.getByTestId('display-name')).toHaveTextContent(formatMessage(hookProps.displayName));
expect(screen.getByTestId('filter-value')).toHaveTextContent(`: ${hookProps.value}`);
const button = screen.getByRole('button', { name: /close/i });
await user.click(button);
expect(handleClose).toHaveBeenCalledWith(hookProps.connectedFilters);
testDisplayName();
testCloseButton();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('value is present in the badge', () => {
expect(el.instance.findByTestId('filter-value')[0].children[0].el).toBe(`: ${hookProps.value}`);
});
});
});

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBadge render do not hide value snapshot 1`] = `
<div>
<span
className="badge badge-info"
>
<span
data-testid="display-name"
>
a common name
</span>
<span
data-testid="filter-value"
>
: a common value
</span>
<Button
aria-label="close"
className="btn-info"
onClick={
{
"handleClose": [
"some",
"filters",
],
}
}
>
<span
aria-hidden="true"
>
×
</span>
</Button>
</span>
<br />
</div>
`;
exports[`FilterBadge render hide Value snapshot 1`] = `
<div>
<span
className="badge badge-info"
>
<span
data-testid="display-name"
>
a common name
</span>
<span
data-testid="filter-value"
/>
<Button
aria-label="close"
className="btn-info"
onClick={
{
"handleClose": [
"some",
"filters",
],
}
}
>
<span
aria-hidden="true"
>
×
</span>
</Button>
</span>
<br />
</div>
`;

View File

@@ -0,0 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterBadges component snapshot - has a filterbadge with handleClose for each filter in badgeOrder 1`] = `
<div>
<FilterBadge
filterName="filter1"
handleClose={[MockFunction this.props.handleClose]}
key="filter1"
/>
<FilterBadge
filterName="filter2"
handleClose={[MockFunction this.props.handleClose]}
key="filter2"
/>
<FilterBadge
filterName="filter3"
handleClose={[MockFunction this.props.handleClose]}
key="filter3"
/>
</div>
`;

View File

@@ -1,10 +1,11 @@
/* eslint-disable import/no-extraneous-dependencies */
/* eslint-disable import/no-named-as-default */
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { shallow } from '@edx/react-unit-test-utils';
import FilterBadges from '.';
import FilterBadge from './FilterBadge';
jest.mock('./FilterBadge', () => 'FilterBadge');
const order = ['filter1', 'filter2', 'filter3'];
jest.mock('data/constants/filters', () => ({
@@ -12,24 +13,24 @@ jest.mock('data/constants/filters', () => ({
badgeOrder: order,
}));
// eslint-disable-next-line react/button-has-type
jest.mock('./FilterBadge', () => jest.fn(({ filterName, handleClose }) => <button onClick={handleClose}>{filterName}</button>));
const handleClose = jest.fn();
describe('FilterBadges', () => {
describe('component', () => {
it('has a filterbadge with handleClose for each filter in badgeOrder', async () => {
render(<FilterBadges handleClose={handleClose} />);
const user = userEvent.setup();
const badge1 = screen.getByText(order[0]);
const badge2 = screen.getByText(order[1]);
const badge3 = screen.getByText(order[2]);
expect(badge1).toBeInTheDocument();
expect(badge2).toBeInTheDocument();
expect(badge3).toBeInTheDocument();
await user.click(badge1);
expect(handleClose).toHaveBeenCalled();
let el;
let handleClose;
beforeEach(() => {
handleClose = jest.fn().mockName('this.props.handleClose');
el = shallow(<FilterBadges handleClose={handleClose} />);
});
test('snapshot - has a filterbadge with handleClose for each filter in badgeOrder', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('has a filterbadge with handleClose for each filter in badgeOrder', () => {
const badgeProps = el.instance.findByType(FilterBadge).map(badgeEl => badgeEl.props);
// key prop is not rendered by react
expect(badgeProps[0]).toMatchObject({ filterName: order[0], handleClose });
expect(badgeProps[1]).toMatchObject({ filterName: order[1], handleClose });
expect(badgeProps[2]).toMatchObject({ filterName: order[2], handleClose });
expect(badgeProps.length).toEqual(3);
});
});
});

View File

@@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilterMenuToggle component render snapshot 1`] = `
<Button
className="btn-primary align-self-start"
id="edit-filters-btn"
onClick={[MockFunction hooks.toggleFilterMenu]}
>
<Icon
className="mr-1"
src="FilterAlt"
/>
Edit Filters
</Button>
`;

View File

@@ -1,11 +1,13 @@
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { thunkActions } from 'data/redux/hooks';
import FilterMenuToggle from '.';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('data/redux/hooks', () => ({
thunkActions: {
@@ -20,23 +22,26 @@ jest.mock('data/redux/hooks', () => ({
const toggleFilterMenu = jest.fn().mockName('hooks.toggleFilterMenu');
thunkActions.app.filterMenu.useToggleMenu.mockReturnValue(toggleFilterMenu);
let el;
describe('FilterMenuToggle component', () => {
beforeEach(() => {
jest.clearAllMocks();
renderWithIntl(<FilterMenuToggle />);
el = shallow(<FilterMenuToggle />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(thunkActions.app.filterMenu.useToggleMenu).toHaveBeenCalled();
});
});
describe('renders', () => {
it('button and triggers click', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: formatMessage(messages.editFilters) });
expect(button).toBeInTheDocument();
await user.click(button);
expect(toggleFilterMenu).toHaveBeenCalled();
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.type).toEqual('Button');
expect(el.instance.props.onClick).toEqual(toggleFilterMenu);
expect(el.instance.children[2].el).toContain(formatMessage(messages.editFilters));
});
});
});

View File

@@ -0,0 +1,23 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`FilteredUsersLabel component render snapshot 1`] = `
<format-message-function
message={
{
"defaultMessage": "Showing {filteredUsers} of {totalUsers} total learners",
"description": "Users visibility label",
"id": "gradebook.GradesTab.usersVisibilityLabel",
}
}
values={
{
"filteredUsers": <BoldText
text={100}
/>,
"totalUsers": <BoldText
text={123}
/>,
}
}
/>
`;

View File

@@ -1,9 +1,13 @@
import { screen } from '@testing-library/react';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { selectors } from 'data/redux/hooks';
import FilteredUsersLabel from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
import FilteredUsersLabel, { BoldText } from '.';
import messages from './messages';
jest.mock('data/redux/hooks', () => ({
selectors: {
@@ -19,29 +23,34 @@ const userCounts = {
};
selectors.grades.useUserCounts.mockReturnValue(userCounts);
let el;
describe('FilteredUsersLabel component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<FilteredUsersLabel />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
renderWithIntl(<FilteredUsersLabel />);
expect(selectors.grades.useUserCounts).toHaveBeenCalled();
});
});
describe('render', () => {
it('null render if totalUsersCount is 0', () => {
test('null render if totalUsersCount is 0', () => {
selectors.grades.useUserCounts.mockReturnValueOnce({
...userCounts,
totalUsersCount: 0,
});
const { container } = renderWithIntl(<FilteredUsersLabel />);
expect(container.firstChild).toBeNull();
expect(shallow(<FilteredUsersLabel />).isEmptyRender()).toEqual(true);
});
it('renders users count correctly', () => {
renderWithIntl(<FilteredUsersLabel />);
expect(screen.getByText((text) => text.includes(userCounts.filteredUsersCount))).toBeInTheDocument();
expect(screen.getByText((text) => text.includes(userCounts.totalUsersCount))).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance).toMatchObject(shallow(formatMessage(messages.visibilityLabel, {
filteredUsers: <BoldText text={userCounts.filteredUsersCount} />,
totalUsers: <BoldText text={userCounts.totalUsersCount} />,
})));
});
});
});

View File

@@ -1,9 +1,11 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import Fields from './Fields';
describe('Gradebook Table Fields', () => {
describe('Username', () => {
let el;
const username = 'MyNameFromHere';
describe('with external_user_key', () => {
const props = {
@@ -11,32 +13,40 @@ describe('Gradebook Table Fields', () => {
userKey: 'My name from another land',
};
beforeEach(() => {
render(<Fields.Username {...props} />);
el = shallow(<Fields.Username {...props} />);
});
it('wraps external user key and username', () => {
const usernameField = screen.getByText(username);
expect(usernameField).toBeInTheDocument();
const userKeyField = screen.getByText(props.userKey);
expect(userKeyField).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('wraps external user key and username', () => {
expect(el.instance.findByType('span')[0].el).toMatchSnapshot();
const content = el.instance.findByType('span')[0].children[0];
expect(content.children[0].children[0].el).toEqual(username);
expect(content.children[1].children[0].el).toEqual(props.userKey);
});
});
describe('without external_user_key', () => {
beforeEach(() => {
render(<Fields.Username username={username} />);
el = shallow(<Fields.Username username={username} />);
});
it('wraps username only', () => {
const usernameField = screen.getByText(username);
expect(usernameField).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('wraps username only', () => {
const content = el.instance.findByType('span')[0].children[0];
expect(content.children[0].children[0].el).toEqual(username);
expect(content.children).toHaveLength(1);
});
});
});
describe('Text', () => {
const value = 'myTag@place.com';
it('wraps entry value', () => {
render(<Fields.Text value={value} />);
const textElement = screen.getByText(value);
expect(textElement).toBeInTheDocument();
test('snapshot', () => {
expect(shallow(<Fields.Text value={value} />).snapshot).toMatchSnapshot();
});
test('wraps entry value', () => {
expect(shallow(<Fields.Text value={value} />).instance.children[0].el).toEqual(value);
});
});
});

View File

@@ -1,5 +1,5 @@
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { selectors, thunkActions } from 'data/redux/hooks';
import transforms from 'data/redux/transforms';
@@ -47,6 +47,7 @@ selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
thunkActions.app.useSetModalStateFromTable.mockReturnValue(setModalState);
transforms.grades.subsectionGrade.mockReturnValue(subsectionGrade);
let el;
let out;
describe('GradeButton', () => {
beforeEach(() => {
@@ -97,24 +98,23 @@ describe('GradeButton', () => {
describe('frozen grades', () => {
beforeEach(() => {
hookSpy.mockReturnValue({ ...hookProps, areGradesFrozen: true });
render(<GradeButton {...props} />);
el = shallow(<GradeButton {...props} />);
});
it('renders only labels', () => {
const label = screen.getByText(hookProps.label);
expect(label).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.el).toEqual(hookProps.label);
});
});
describe('not frozen grades', () => {
beforeEach(() => {
hookSpy.mockReturnValue(hookProps);
render(<GradeButton {...props} />);
el = shallow(<GradeButton {...props} />);
});
it('renders button', async () => {
const user = userEvent.setup();
const button = screen.getByRole('button', { name: hookProps.label });
expect(button).toBeInTheDocument();
await user.click(button);
expect(hookProps.onClick).toHaveBeenCalled();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
expect(el.instance.type).toEqual('Button');
expect(el.instance.props.onClick).toEqual(hookProps.onClick);
expect(el.instance.children[0].el).toEqual(hookProps.label);
});
});
});

View File

@@ -1,8 +1,10 @@
import { screen } from '@testing-library/react';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { getLocale } from '@edx/frontend-platform/i18n';
import { OverlayTrigger } from '@openedx/paragon';
import LabelReplacements from './LabelReplacements';
import messages from './messages';
import { renderWithIntl } from '../../../testUtilsExtra';
const {
TotalGradeLabelReplacement,
@@ -10,36 +12,52 @@ const {
MastersOnlyLabelReplacement,
} = LabelReplacements;
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
getLocale: jest.fn(),
isRtl: jest.fn(),
jest.mock('@openedx/paragon', () => ({
Icon: () => 'Icon',
OverlayTrigger: () => 'OverlayTrigger',
Tooltip: () => 'Tooltip',
}));
describe('LabelReplacements', () => {
describe('TotalGradeLabelReplacement', () => {
getLocale.mockImplementation(() => 'en');
renderWithIntl(<TotalGradeLabelReplacement />);
it('displays overlay tooltip', () => {
const tooltip = screen.getByText(messages.totalGradePercentage.defaultMessage);
expect(tooltip).toBeInTheDocument();
let el;
beforeEach(() => {
el = shallow(<TotalGradeLabelReplacement />);
});
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('displays overlay tooltip', () => {
expect(el.instance.findByType(OverlayTrigger)[0].props.overlay).toMatchSnapshot();
});
});
describe('UsernameLabelReplacement', () => {
it('renders correctly', () => {
renderWithIntl(<UsernameLabelReplacement />);
expect(screen.getByText(messages.usernameHeading.defaultMessage)).toBeInTheDocument();
test('snapshot', () => {
expect(shallow(<UsernameLabelReplacement />).snapshot).toMatchSnapshot();
});
});
describe('MastersOnlyLabelReplacement', () => {
it('renders correctly', () => {
test('snapshot', () => {
const message = {
id: 'id',
defaultMessage: 'defaultMessAge',
description: 'desCripTion',
};
renderWithIntl(<MastersOnlyLabelReplacement {...message} />);
expect(screen.getByText(message.defaultMessage)).toBeInTheDocument();
expect(shallow(<MastersOnlyLabelReplacement {...message} />).snapshot).toMatchSnapshot();
});
});
});
describe('snapshot', () => {
let el;
test('right to left overlay placement', () => {
getLocale.mockImplementation(() => 'en');
el = shallow(<TotalGradeLabelReplacement />);
expect(el.snapshot).toMatchSnapshot();
});
test('left to right overlay placement', () => {
getLocale.mockImplementation(() => 'ar');
el = shallow(<TotalGradeLabelReplacement />);
expect(el.snapshot).toMatchSnapshot();
});
});

View File

@@ -0,0 +1,75 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Gradebook Table Fields Text snapshot 1`] = `
<span
className="wrap-text-in-cell"
>
myTag@place.com
</span>
`;
exports[`Gradebook Table Fields Username with external_user_key snapshot 1`] = `
<div>
<span
className="wrap-text-in-cell"
>
<div>
<div>
MyNameFromHere
</div>
<div
className="student-key"
>
My name from another land
</div>
</div>
</span>
</div>
`;
exports[`Gradebook Table Fields Username with external_user_key wraps external user key and username 1`] = `
{
"children": [
{
"children": [
{
"children": [
"MyNameFromHere",
],
"props": {},
"type": "div",
},
{
"children": [
"My name from another land",
],
"props": {
"className": "student-key",
},
"type": "div",
},
],
"props": {},
"type": "div",
},
],
"props": {
"className": "wrap-text-in-cell",
},
"type": "span",
}
`;
exports[`Gradebook Table Fields Username without external_user_key snapshot 1`] = `
<div>
<span
className="wrap-text-in-cell"
>
<div>
<div>
MyNameFromHere
</div>
</div>
</span>
</div>
`;

View File

@@ -0,0 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradeButton component frozen grades snapshot 1`] = `"test-label"`;
exports[`GradeButton component not frozen grades snapshot 1`] = `
<Button
className="btn-header grade-button"
onClick={[MockFunction hooks.onClick]}
variant="link"
>
test-label
</Button>
`;

View File

@@ -0,0 +1,140 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LabelReplacements MastersOnlyLabelReplacement snapshot 1`] = `
<div>
defaultMessAge
<span
className="font-weight-normal"
>
*
</span>
</div>
`;
exports[`LabelReplacements TotalGradeLabelReplacement displays overlay tooltip 1`] = `
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
`;
exports[`LabelReplacements TotalGradeLabelReplacement snapshot 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="left"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;
exports[`LabelReplacements UsernameLabelReplacement snapshot 1`] = `
<div>
<div>
Username
</div>
<div
className="font-weight-normal student-key"
>
Student Key
<span
className="font-weight-normal"
>
*
</span>
</div>
</div>
`;
exports[`snapshot left to right overlay placement 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="right"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;
exports[`snapshot right to left overlay placement 1`] = `
<div>
<OverlayTrigger
key="left-basic"
overlay={
<Tooltip
id="course-grade-tooltip"
>
Total Grade values are always displayed as a percentage
</Tooltip>
}
placement="left"
trigger={
[
"hover",
"focus",
]
}
>
<div>
Total Grade (%)
<div
id="courseGradeTooltipIcon"
>
<Icon
className="fa fa-info-circle"
screenReaderText="Total Grade values are always displayed as a percentage"
/>
</div>
</div>
</OverlayTrigger>
</div>
`;

View File

@@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradebookTable snapshot 1`] = `
<div
className="gradebook-container"
>
<DataTable
RowStatusComponent={[MockFunction hooks.nullMethod]}
columns={
[
"some",
"columns",
]
}
data={
[
"some",
"data",
]
}
hasFixedColumnWidths={true}
itemCount={3}
rowHeaderColumnKey="username"
>
<DataTable.TableControlBar />
<DataTable.Table />
<DataTable.EmptyTable
content="empty-table-content"
/>
</DataTable>
</div>
`;

View File

@@ -1,32 +1,47 @@
import React from 'react';
import { Headings } from 'data/constants/grades';
import { shallow } from '@edx/react-unit-test-utils';
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { getLocalizedPercentSign } from 'i18n/utils';
import { selectors } from 'data/redux/hooks';
import transforms from 'data/redux/transforms';
import { Headings } from 'data/constants/grades';
import LabelReplacements from './LabelReplacements';
import Fields from './Fields';
import GradeButton from './GradeButton';
import { initializeMocks, render } from '../../../testUtilsExtra';
import * as hooks from './hooks';
import messages from './messages';
let mockUseAllGrades;
let mockUseGetHeadings;
import useGradebookTableData from './hooks';
jest.mock('i18n/utils', () => ({
getLocalizedPercentSign: () => '%',
}));
jest.mock('./GradeButton', () => 'GradeButton');
jest.mock('./Fields', () => jest.requireActual('testUtils').mockNestedComponents({
Username: 'Fields.Username',
Text: 'Fields.Text',
}));
jest.mock('./LabelReplacements', () => jest.requireActual('testUtils').mockNestedComponents({
TotalGradeLabelReplacement: 'LabelReplacements.TotalGradeLabelReplacement',
UsernameLabelReplacement: 'LabelReplacements.UsernameLabelReplacement',
MastersOnlyLabelReplacement: 'LabelReplacements.MastersOnlyLabelReplacement',
}));
jest.mock('data/redux/hooks', () => ({
selectors: {
grades: { useAllGrades: () => mockUseAllGrades() },
root: { useGetHeadings: () => mockUseGetHeadings() },
grades: { useAllGrades: jest.fn() },
root: { useGetHeadings: jest.fn() },
},
}));
jest.mock('data/redux/transforms', () => ({
grades: { roundGrade: jest.fn((val) => val) },
grades: { roundGrade: jest.fn() },
}));
jest.mock('i18n/utils', () => ({ getLocalizedPercentSign: () => '%' }));
jest.mock('./Fields', () => ({ Username: () => null, Text: () => null }));
jest.mock('./GradeButton', () => ({ __esModule: true, default: () => null }));
jest.mock('./LabelReplacements', () => ({
TotalGradeLabelReplacement: () => null,
UsernameLabelReplacement: () => null,
MastersOnlyLabelReplacement: () => null,
}));
const roundGrade = grade => grade * 20;
transforms.grades.roundGrade.mockImplementation(roundGrade);
const subsectionLabels = [
'subsectionLabel1',
@@ -70,9 +85,7 @@ const allGrades = [
],
},
];
const testHeading = 'test-heading-value';
const headings = [
Headings.totalGrade,
Headings.username,
@@ -80,60 +93,100 @@ const headings = [
Headings.fullName,
testHeading,
];
selectors.grades.useAllGrades.mockReturnValue(allGrades);
selectors.root.useGetHeadings.mockReturnValue(headings);
describe('useGradebookTableData hook', () => {
beforeAll(() => {
mockUseAllGrades = jest.fn();
mockUseGetHeadings = jest.fn();
});
let out;
describe('useGradebookTableData', () => {
beforeEach(() => {
mockUseAllGrades.mockReset();
mockUseGetHeadings.mockReset();
jest.clearAllMocks();
out = useGradebookTableData();
});
let hookResult;
const TestComponent = () => {
hookResult = hooks.useGradebookTableData();
return null;
};
beforeEach(() => {
initializeMocks();
hookResult = null;
mockUseAllGrades.mockReturnValue([]);
mockUseGetHeadings.mockReturnValue([]);
});
it('returns expected structure with empty data', () => {
render(<TestComponent />);
expect(hookResult).toEqual({
columns: [],
data: [],
grades: [],
nullMethod: expect.any(Function),
emptyContent: expect.any(String),
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
it('initializes redux hooks', () => {
expect(selectors.grades.useAllGrades).toHaveBeenCalled();
expect(selectors.root.useGetHeadings).toHaveBeenCalled();
});
});
it('nullMethod returns null', () => {
render(<TestComponent />);
expect(hookResult.nullMethod()).toBeNull();
});
it('returns expected structure with grades and headings data', () => {
mockUseAllGrades.mockReturnValue(allGrades);
mockUseGetHeadings.mockReturnValue(headings);
render(<TestComponent />);
expect(hookResult.columns.length).toBe(headings.length);
expect(hookResult.columns[0].accessor).toEqual(headings[0]);
expect(hookResult.data.length).toBe(allGrades.length);
expect(hookResult.data[0]).toHaveProperty(Headings.username);
expect(hookResult.grades).toEqual(allGrades);
expect(hookResult.nullMethod()).toBeNull();
expect(hookResult.emptyContent).toBe(messages.noResultsFound.defaultMessage);
expect(mockUseAllGrades).toHaveBeenCalled();
expect(mockUseGetHeadings).toHaveBeenCalled();
describe('output', () => {
describe('columns', () => {
test('total grade heading produces TotalGradeLabelReplacement label', () => {
const { Header, accessor } = out.columns[0];
expect(accessor).toEqual(headings[0]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.TotalGradeLabelReplacement />),
);
});
test('username heading produces UsernameLabelReplacement', () => {
const { Header, accessor } = out.columns[1];
expect(accessor).toEqual(headings[1]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.UsernameLabelReplacement />),
);
});
test('email heading replaces with email heading message', () => {
const { Header, accessor } = out.columns[2];
expect(accessor).toEqual(headings[2]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.emailHeading} />),
);
});
test('fullName heading replaces with fullName heading message', () => {
const { Header, accessor } = out.columns[3];
expect(accessor).toEqual(headings[3]);
expect(shallow(Header)).toMatchObject(
shallow(<LabelReplacements.MastersOnlyLabelReplacement {...messages.fullNameHeading} />),
);
});
test('other headings are passed through', () => {
const { Header, accessor } = out.columns[4];
expect(accessor).toEqual(headings[4]);
expect(Header).toEqual(headings[4]);
});
});
describe('data', () => {
test('username field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.username]).toMatchObject(
<Fields.Username username={entry.username} userKey={entry.external_user_key} />,
);
});
});
test('email field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.email]).toMatchObject(
<Fields.Text value={entry.email} />,
);
});
});
test('totalGrade field', () => {
allGrades.forEach((entry, index) => {
expect(out.data[index][Headings.totalGrade]).toEqual(
`${roundGrade(entry.percent * 100)}${getLocalizedPercentSign()}`,
);
});
});
test('section breakdown', () => {
allGrades.forEach((entry, gradeIndex) => {
subsectionLabels.forEach((label, labelIndex) => {
expect(out.data[gradeIndex][label]).toMatchObject(
<GradeButton entry={entry} subsection={entry.section_breakdown[labelIndex]} />,
);
});
});
});
});
it('forwards grades from redux', () => {
expect(out.grades).toEqual(allGrades);
});
test('nullMethod returns null', () => {
expect(out.nullMethod()).toEqual(null);
});
test('emptyContent', () => {
expect(out.emptyContent).toEqual(formatMessage(messages.noResultsFound));
});
});
});

View File

@@ -1,42 +1,39 @@
import { screen } from '@testing-library/react';
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { DataTable } from '@openedx/paragon';
import useGradebookTableData from './hooks';
import GradebookTable from '.';
import { renderWithIntl } from '../../../testUtilsExtra';
jest.mock('./hooks', () => jest.fn());
const hookProps = {
columns: [{ Header: 'Username', accessor: 'username' }, { Header: 'Email', accessor: 'email' }, { Header: 'Total Grade', accessor: 'totalGrade' }],
data: [{ username: 'instructor', email: 'instructor@example.com', totalGrade: '100' }, { username: 'student', email: 'student@example.com', totalGrade: '90' }],
columns: ['some', 'columns'],
data: ['some', 'data'],
grades: ['a', 'few', 'grades'],
nullMethod: jest.fn().mockName('hooks.nullMethod'),
emptyContent: 'empty-table-content',
};
useGradebookTableData.mockReturnValue(hookProps);
let el;
describe('GradebookTable', () => {
it('renders Datatable correctly', () => {
useGradebookTableData.mockReturnValue(hookProps);
renderWithIntl(<GradebookTable />);
expect(useGradebookTableData).toHaveBeenCalled();
const headers = screen.getAllByRole('columnheader');
expect(headers).toHaveLength(3);
expect(headers[0]).toHaveTextContent(hookProps.columns[0].Header);
expect(headers[1]).toHaveTextContent(hookProps.columns[1].Header);
expect(headers[2]).toHaveTextContent(hookProps.columns[2].Header);
const rows = screen.getAllByRole('row');
expect(rows).toHaveLength(3);
expect(screen.getByText(hookProps.data[0].username)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[0].email)).toBeInTheDocument();
expect(screen.getByText(hookProps.data[0].totalGrade)).toBeInTheDocument();
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<GradebookTable />);
});
it('renders empty table content when no data is available', () => {
useGradebookTableData.mockReturnValue({
...hookProps,
data: [],
grades: [],
});
renderWithIntl(<GradebookTable />);
expect(screen.getByText(hookProps.emptyContent)).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Datatable props', () => {
const datatable = el.instance.findByType(DataTable)[0];
const { props } = datatable;
expect(props.columns).toEqual(hookProps.columns);
expect(props.data).toEqual(hookProps.data);
expect(props.itemCount).toEqual(hookProps.grades.length);
expect(props.RowStatusComponent).toEqual(hookProps.nullMethod);
expect(datatable.children[2].type).toEqual('DataTable.EmptyTable');
expect(datatable.children[2].props.content).toEqual(hookProps.emptyContent);
});
});

View File

@@ -0,0 +1,53 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportGradesButton component render Form 1`] = `
<Form
action="test-grade-export-url"
method="post"
>
<Form.Group
controlId="csv"
>
<Form.Control
className="d-none"
data-testid="file-control"
label="Upload Grade CSV"
onChange={[MockFunction props.handleFileInputChange]}
type="file"
/>
</Form.Group>
</Form>
`;
exports[`ImportGradesButton component render snapshot 1`] = `
<Fragment>
<Form
action="test-grade-export-url"
method="post"
>
<Form.Group
controlId="csv"
>
<Form.Control
className="d-none"
data-testid="file-control"
label="Upload Grade CSV"
onChange={[MockFunction props.handleFileInputChange]}
type="file"
/>
</Form.Group>
</Form>
<NetworkButton
className="import-grades-btn"
import={true}
label={
{
"defaultMessage": "Import Grades",
"description": "A labeled button to import grades in the BulkManagement Tab File Upload Form",
"id": "gradebook.GradesView.importGradesBtnText",
}
}
onClick={[MockFunction props.handleClickImportGrades]}
/>
</Fragment>
`;

View File

@@ -12,11 +12,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useRef: jest.fn((val) => ({ current: val, useRef: true })),
}));
let out;
let submitThen;

View File

@@ -1,26 +1,46 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import {
render, screen, initializeMocks,
} from 'testUtilsExtra';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form } from '@openedx/paragon';
import NetworkButton from 'components/NetworkButton';
import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.';
initializeMocks();
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
let el;
let props;
describe('ImportGradesButton component', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<ImportGradesButton />);
beforeAll(() => {
props = {
fileInputRef: { current: null },
gradeExportUrl: 'test-grade-export-url',
handleClickImportGrades: jest.fn().mockName('props.handleClickImportGrades'),
handleFileInputChange: jest.fn().mockName('props.handleFileInputChange'),
};
useImportGradesButtonData.mockReturnValue(props);
el = shallow(<ImportGradesButton />);
});
describe('behavior', () => {
it('initializes hooks', () => {
expect(useImportGradesButtonData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
});
});
describe('render', () => {
test('Form', async () => {
const uploader = screen.getByTestId('file-control');
expect(uploader).toBeInTheDocument();
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('Form', () => {
expect(el.instance.findByType(Form)[0].snapshot).toMatchSnapshot();
expect(el.instance.findByType(Form)[0].props.action).toEqual(props.gradeExportUrl);
expect(el.instance.findByType(Form.Control)[0].props.onChange).toEqual(props.handleFileInputChange);
});
test('import button', () => {
expect(screen.getByRole('button', { name: 'Import Grades' })).toBeInTheDocument();
expect(el.instance.findByType(NetworkButton)[0].props.onClick).toEqual(props.handleClickImportGrades);
});
});
});

View File

@@ -1,8 +1,11 @@
import React from 'react';
import { render } from '@testing-library/react';
import useImportGradesButtonData from './hooks';
import ImportGradesButton from '.';
import { renderWithIntl, screen } from '../../../testUtilsExtra';
jest.unmock('react');
jest.unmock('@openedx/paragon');
jest.mock('components/NetworkButton', () => 'network-button');
jest.mock('./hooks', () => ({ __esModule: true, default: jest.fn() }));
@@ -14,10 +17,11 @@ const props = {
};
useImportGradesButtonData.mockReturnValue(props);
let el;
describe('ImportGradesButton ref test', () => {
it('loads ref from hook', () => {
renderWithIntl(<ImportGradesButton />);
const input = screen.getByTestId('file-control');
el = render(<ImportGradesButton />);
const input = el.getByTestId('file-control');
expect(input).toEqual(props.fileInputRef.current);
});
});

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ImportSuccessToast component render snapshot 1`] = `
<Toast
action="test-action"
onClose={[MockFunction hooks.onClose]}
show="test-show"
>
test-description
</Toast>
`;

View File

@@ -19,18 +19,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const setView = jest.fn().mockName('hooks.setView');
const setShowToast = jest.fn().mockName('hooks.setShowImportSuccessToast');
actions.app.useSetView.mockReturnValue(setView);

View File

@@ -1,88 +1,39 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, initializeMocks, screen } from 'testUtilsExtra';
import ImportSuccessToast from '.';
import useImportSuccessToastData from './hooks';
jest.mock('data/redux/hooks', () => ({
actions: {
app: {
useSetView: jest.fn(),
useSetShowImportSuccessToast: jest.fn(),
},
},
selectors: {
app: {
useShowImportSuccessToast: jest.fn(),
},
},
}));
import ImportSuccessToast from '.';
jest.mock('./hooks', () => jest.fn());
initializeMocks();
const hookProps = {
action: 'test-action',
onClose: jest.fn().mockName('hooks.onClose'),
show: 'test-show',
description: 'test-description',
};
useImportSuccessToastData.mockReturnValue(hookProps);
describe('ImportSuccessToast', () => {
beforeEach(() => {
jest.clearAllMocks();
let el;
describe('ImportSuccessToast component', () => {
beforeAll(() => {
el = shallow(<ImportSuccessToast />);
});
it('renders with show false', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: false,
description: 'Import Successful! Grades will be updated momentarily.',
describe('behavior', () => {
it('initializes component hook', () => {
expect(useImportSuccessToastData).toHaveBeenCalled();
});
render(<ImportSuccessToast />);
const toastRoot = document.getElementById('toast-root');
expect(toastRoot).toBeInTheDocument();
expect(toastRoot).toHaveClass('toast-container');
const toastMessage = screen.queryByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeNull();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
it('renders with show true', () => {
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: jest.fn(),
},
onClose: jest.fn(),
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
render(<ImportSuccessToast />);
const toastMessage = screen.getByText('Import Successful! Grades will be updated momentarily.');
expect(toastMessage).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
it('passes correct props to Toast component', () => {
const mockOnClose = jest.fn();
const mockOnClick = jest.fn();
useImportSuccessToastData.mockReturnValue({
action: {
label: 'View Activity Log',
onClick: mockOnClick,
},
onClose: mockOnClose,
show: true,
description: 'Import Successful! Grades will be updated momentarily.',
test('Toast', () => {
expect(el.instance.type).toEqual('Toast');
expect(el.instance.props.action).toEqual(hookProps.action);
expect(el.instance.props.onClose).toEqual(hookProps.onClose);
expect(el.instance.props.show).toEqual(hookProps.show);
expect(el.instance.children[0].el).toEqual(hookProps.description);
});
const { container } = render(<ImportSuccessToast />);
expect(container).toBeInTheDocument();
expect(useImportSuccessToastData).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,30 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`InterventionsReport component output snapshot 1`] = `
<div>
<h4
className="mt-0"
>
Interventions Report
</h4>
<div
className="d-flex justify-content-between align-items-center"
>
<div
className="intervention-report-description"
>
Need to find students who may be falling behind? Download the interventions report to obtain engagement metrics such as section attempts and visits.
</div>
<NetworkButton
label={
{
"defaultMessage": "Download Interventions",
"description": "The labeled button to download the Intervention report from the Grades View",
"id": "gradebook.GradesView.InterventionsReport.downloadBtn",
}
}
onClick={[MockFunction]}
/>
</div>
</div>
`;

View File

@@ -1,73 +1,42 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import { useIntl } from '@edx/frontend-platform/i18n';
import NetworkButton from 'components/NetworkButton';
import messages from './messages';
import useInterventionsReportData from './hooks';
import InterventionsReport from '.';
jest.mock('components/NetworkButton', () => 'NetworkButton');
jest.mock('./hooks', () => jest.fn());
const useInterventionsReportData = require('./hooks');
const hookProps = { show: true, handleClick: jest.fn() };
useInterventionsReportData.mockReturnValue(hookProps);
initializeMocks();
describe('InterventionsReport', () => {
let el;
describe('InterventionsReport component', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<InterventionsReport />);
});
it('renders without errors when show is true', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
describe('behavior', () => {
it('initializes hooks', () => {
expect(useInterventionsReportData).toHaveBeenCalledWith();
expect(useIntl).toHaveBeenCalledWith();
});
render(<InterventionsReport />);
expect(screen.getByRole('heading', { level: 4 })).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('renders nothing when show is false', () => {
useInterventionsReportData.mockReturnValue({
show: false,
handleClick: jest.fn(),
describe('output', () => {
it('does now render if show is false', () => {
useInterventionsReportData.mockReturnValueOnce({ ...hookProps, show: false });
el = shallow(<InterventionsReport />);
expect(el.isEmptyRender()).toEqual(true);
});
render(<InterventionsReport />);
expect(screen.queryByRole('heading')).not.toBeInTheDocument();
expect(screen.queryByText('Interventions Report')).not.toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('calls useInterventionsReportData hook', () => {
useInterventionsReportData.mockReturnValue({
show: true,
handleClick: jest.fn(),
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
const btnProps = el.instance.findByType(NetworkButton)[0].props;
expect(btnProps.label).toEqual(messages.downloadBtn);
expect(btnProps.onClick).toEqual(hookProps.handleClick);
});
render(<InterventionsReport />);
expect(useInterventionsReportData).toHaveBeenCalled();
});
it('renders with correct content when show is true', () => {
const mockReportData = {
show: true,
handleClick: jest.fn(),
};
useInterventionsReportData.mockReturnValue(mockReportData);
render(<InterventionsReport />);
expect(screen.getByText('Interventions Report')).toBeInTheDocument();
expect(
screen.getByText(/Need to find students who may be falling behind/),
).toBeInTheDocument();
const networkButton = document.querySelector('networkbutton');
expect(networkButton).toBeInTheDocument();
expect(useInterventionsReportData).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,37 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PageButtons component render snapshot 1`] = `
<div
className="d-flex justify-content-center"
style={
{
"paddingBottom": "20px",
}
}
>
<Button
disabled="prev-disabled"
onClick={[MockFunction hooks.prev.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
prev-text
</Button>
<Button
disabled="next-disabled"
onClick={[MockFunction hooks.next.onClick]}
style={
{
"margin": "20px",
}
}
variant="outline-primary"
>
next-text
</Button>
</div>
`;

View File

@@ -15,18 +15,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const gradeData = { nextPage: 'test-next-page', prevPage: 'test-prev-page' };
selectors.grades.useGradeData.mockReturnValue(gradeData);

View File

@@ -1,6 +1,7 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import { Button } from '@openedx/paragon';
import usePageButtonsData from './hooks';
import PageButtons from '.';
@@ -9,53 +10,44 @@ jest.mock('./hooks', () => jest.fn());
const hookProps = {
prev: {
disabled: false,
disabled: 'prev-disabled',
onClick: jest.fn().mockName('hooks.prev.onClick'),
text: 'prev-text',
},
next: {
disabled: false,
disabled: 'next-disabled',
onClick: jest.fn().mockName('hooks.next.onClick'),
text: 'next-text',
},
};
usePageButtonsData.mockReturnValue(hookProps);
let el;
describe('PageButtons component', () => {
beforeAll(() => {
beforeEach(() => {
jest.clearAllMocks();
initializeMocks();
el = shallow(<PageButtons />);
});
describe('renders enabled buttons', () => {
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
});
test('prev button enabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeEnabled();
});
test('next button enabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeEnabled();
describe('behavior', () => {
it('initializes component hooks', () => {
expect(usePageButtonsData).toHaveBeenCalled();
});
});
describe('renders disabled buttons', () => {
beforeAll(() => {
hookProps.prev.disabled = true;
hookProps.next.disabled = true;
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
beforeEach(() => {
usePageButtonsData.mockReturnValue(hookProps);
render(<PageButtons />);
test('prev button', () => {
const button = el.instance.findByType(Button)[0];
expect(button.props.disabled).toEqual(hookProps.prev.disabled);
expect(button.props.onClick).toEqual(hookProps.prev.onClick);
expect(button.children[0].el).toEqual(hookProps.prev.text);
});
test('prev button disabled', () => {
expect(screen.getByText(hookProps.next.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.prev.text)).toBeDisabled();
});
test('next button disabled', () => {
expect(screen.getByText(hookProps.prev.text)).toBeInTheDocument();
expect(screen.getByText(hookProps.next.text)).toBeDisabled();
test('next button', () => {
const button = el.instance.findByType(Button)[1];
expect(button.props.disabled).toEqual(hookProps.next.disabled);
expect(button.props.onClick).toEqual(hookProps.next.onClick);
expect(button.children[0].el).toEqual(hookProps.next.text);
});
});
});

View File

@@ -0,0 +1,28 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ScoreViewInput component render snapshot 1`] = `
<Form.Group
controlId="ScoreView"
>
<Form.Label>
Score View
:
</Form.Label>
<Form.Control
as="select"
onChange={[MockFunction hooks.toggleGradeFormat]}
value="test-grade-format"
>
<option
value="percent"
>
Percent
</option>
<option
value="absolute"
>
Absolute
</option>
</Form.Control>
</Form.Group>
`;

View File

@@ -1,237 +1,67 @@
import React from 'react';
import { shallow } from '@edx/react-unit-test-utils';
import { render, screen, initializeMocks } from 'testUtilsExtra';
import userEvent from '@testing-library/user-event';
import { useIntl } from '@edx/frontend-platform/i18n';
import { GradeFormats } from 'data/constants/grades';
import { ScoreViewInput } from '.';
import { formatMessage } from 'testUtils';
import { actions, selectors } from 'data/redux/hooks';
import ScoreViewInput from '.';
import messages from './messages';
jest.mock('data/redux/hooks', () => ({
actions: {
grades: {
useToggleGradeFormat: jest.fn(),
},
grades: { useToggleGradeFormat: jest.fn() },
},
selectors: {
grades: {
useGradeData: jest.fn(),
},
grades: { useGradeData: jest.fn() },
},
}));
const { actions, selectors } = require('data/redux/hooks');
initializeMocks();
describe('ScoreViewInput', () => {
const mockToggleFormat = jest.fn();
const toggleGradeFormat = jest.fn().mockName('hooks.toggleGradeFormat');
actions.grades.useToggleGradeFormat.mockReturnValue(toggleGradeFormat);
const gradeFormat = 'test-grade-format';
selectors.grades.useGradeData.mockReturnValue({ gradeFormat });
let el;
describe('ScoreViewInput component', () => {
beforeEach(() => {
jest.clearAllMocks();
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
el = shallow(<ScoreViewInput />);
});
describe('behavior', () => {
it('initializes intl hook', () => {
expect(useIntl).toHaveBeenCalled();
});
actions.grades.useToggleGradeFormat.mockReturnValue(mockToggleFormat);
});
it('renders without errors', () => {
render(<ScoreViewInput />);
expect(document.body).toBeInTheDocument();
});
it('renders form group with correct label', () => {
render(<ScoreViewInput />);
expect(screen.getByLabelText(/score view/i)).toBeInTheDocument();
});
it('renders select element with correct options', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /percent/i }),
).toBeInTheDocument();
expect(
screen.getByRole('option', { name: /absolute/i }),
).toBeInTheDocument();
});
it('displays correct selected value for percent format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'percent',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
});
it('displays correct selected value for absolute format', () => {
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
});
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
it('calls toggle function when selection changes', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledTimes(1);
});
describe('accessibility', () => {
it('has proper form structure', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const label = screen.getByText(/score view/i);
expect(select).toBeInTheDocument();
expect(label).toBeInTheDocument();
});
it('has accessible label association', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toHaveAccessibleName(/score view/i);
});
it('has correct control ID for accessibility', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveAttribute('id', 'ScoreView');
it('initializes redux hooks', () => {
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalled();
expect(selectors.grades.useGradeData).toHaveBeenCalled();
});
});
describe('form control behavior', () => {
it('renders as a select element', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select.tagName).toBe('SELECT');
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('has correct option values', () => {
render(<ScoreViewInput />);
const percentOption = screen.getByRole('option', { name: /percent/i });
const absoluteOption = screen.getByRole('option', { name: /absolute/i });
expect(percentOption).toHaveValue('percent');
expect(absoluteOption).toHaveValue('absolute');
test('label', () => {
const label = el.instance.children[0];
expect(label.children[0].el).toEqual(`${formatMessage(messages.scoreView)}`);
});
it('has exactly two options', () => {
render(<ScoreViewInput />);
const options = screen.getAllByRole('option');
expect(options).toHaveLength(2);
});
});
describe('redux integration', () => {
it('uses grade data selector hook', () => {
render(<ScoreViewInput />);
expect(selectors.grades.useGradeData).toHaveBeenCalledTimes(1);
});
it('uses toggle grade format action hook', () => {
render(<ScoreViewInput />);
expect(actions.grades.useToggleGradeFormat).toHaveBeenCalledTimes(1);
});
it('responds to different grade format values', () => {
const { rerender } = render(<ScoreViewInput />);
let select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('percent');
selectors.grades.useGradeData.mockReturnValue({
gradeFormat: 'absolute',
describe('form control', () => {
let control;
beforeEach(() => {
control = el.instance.children;
});
test('value and onChange from redux hooks', () => {
expect(control[1].props.value).toEqual(gradeFormat);
expect(control[1].props.onChange).toEqual(toggleGradeFormat);
});
test('absolute and percent options', () => {
const { children } = control[1];
expect(children[0].props.value).toEqual(GradeFormats.percent);
expect(children[0].children[0].el).toEqual(formatMessage(messages.percent));
expect(children[1].props.value).toEqual(GradeFormats.absolute);
expect(children[1].children[0].el).toEqual(formatMessage(messages.absolute));
});
rerender(<ScoreViewInput />);
select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveValue('absolute');
});
});
describe('user interactions', () => {
it('handles option selection', async () => {
render(<ScoreViewInput />);
const user = userEvent.setup();
const select = screen.getByRole('combobox', { name: /score view/i });
await user.selectOptions(select, 'absolute');
expect(mockToggleFormat).toHaveBeenCalledWith(expect.any(Object));
});
it('maintains state consistency', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
const percentOption = screen.getByRole('option', { name: /percent/i });
expect(select).toHaveValue('percent');
expect(percentOption).toBeInTheDocument();
});
});
describe('internationalization', () => {
it('displays localized label text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Score View:')).toBeInTheDocument();
});
it('displays localized option text', () => {
render(<ScoreViewInput />);
expect(screen.getByText('Percent')).toBeInTheDocument();
expect(screen.getByText('Absolute')).toBeInTheDocument();
});
});
describe('component structure', () => {
it('uses proper Bootstrap form classes', () => {
render(<ScoreViewInput />);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(select).toHaveClass('form-control');
});
it('renders within form group structure', () => {
render(<ScoreViewInput />);
const label = screen.getByText(/score view/i);
const select = screen.getByRole('combobox', { name: /score view/i });
expect(label).toBeInTheDocument();
expect(select).toBeInTheDocument();
expect(select).toHaveAccessibleName(expect.stringMatching(/score view/i));
});
});
});

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SearchControls component render snapshot 1`] = `
<div
className="search-container"
>
<SearchField
inputLabel="test-input-label"
onBlur={[MockFunction hooks.onBlur]}
onClear={[MockFunction hooks.onClear]}
onSubmit={[MockFunction hooks.onSubmit]}
value="test-search-value"
/>
<small
className="form-text text-muted search-help-text"
>
test-hint-text
</small>
</div>
`;

View File

@@ -18,18 +18,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const searchValue = 'test-search-value';
selectors.app.useSearchValue.mockReturnValue(searchValue);
const setSearchValue = jest.fn();

View File

@@ -21,7 +21,7 @@ export const SearchControls = () => {
<div className="search-container">
<SearchField
onSubmit={onSubmit}
label={inputLabel}
inputLabel={inputLabel}
onBlur={onBlur}
onClear={onClear}
value={searchValue}

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { initializeMocks, render, screen } from 'testUtilsExtra';
import { shallow } from '@edx/react-unit-test-utils';
import { SearchField } from '@openedx/paragon';
import useSearchControlsData from './hooks';
import SearchControls from '.';
@@ -15,19 +17,32 @@ const hookProps = {
hintText: 'test-hint-text',
};
useSearchControlsData.mockReturnValue(hookProps);
let el;
describe('SearchControls component', () => {
beforeEach(() => {
initializeMocks();
render(<SearchControls />);
jest.clearAllMocks();
el = shallow(<SearchControls />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
expect(useSearchControlsData).toHaveBeenCalled();
});
});
describe('render', () => {
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
test('search field', () => {
expect(screen.getByLabelText(hookProps.inputLabel)).toBeInTheDocument();
expect(screen.getByRole('searchbox')).toHaveValue(hookProps.searchValue);
const { props } = el.instance.findByType(SearchField)[0];
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(screen.getByText(hookProps.hintText)).toBeInTheDocument();
expect(el.instance.findByType('small')[0].children[0].el).toEqual(hookProps.hintText);
});
});
});

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { render } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { selectors } from 'data/redux/hooks';
import SpinnerIcon from './SpinnerIcon';
@@ -10,19 +10,26 @@ jest.mock('data/redux/hooks', () => ({
},
}));
selectors.root.useShouldShowSpinner.mockReturnValue(true);
let el;
describe('SpinnerIcon', () => {
beforeEach(() => {
jest.clearAllMocks();
el = shallow(<SpinnerIcon />);
});
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).not.toBeInTheDocument();
describe('behavior', () => {
it('initializes redux hook', () => {
expect(selectors.root.useShouldShowSpinner).toHaveBeenCalled();
});
});
test('displays spinner overlay with spinner icon', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(true);
const { container } = render(<SpinnerIcon />);
expect(container.querySelector('.fa.fa-spinner')).toBeInTheDocument();
describe('component', () => {
it('does not render if show: false', () => {
selectors.root.useShouldShowSpinner.mockReturnValueOnce(false);
el = shallow(<SpinnerIcon />);
expect(el.isEmptyRender()).toEqual(true);
});
test('snapshot - displays spinner overlay with spinner icon', () => {
expect(el.snapshot).toMatchSnapshot();
});
});
});

View File

@@ -0,0 +1,20 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StatusAlerts component render snapshot 1`] = `
<Fragment>
<Alert
onClose={[MockFunction hooks.successBanner.onClose]}
show="hooks.show-success-banner"
variant="success"
>
hooks.success-banner-text
</Alert>
<Alert
dismissible={false}
show="hooks.show-grade-filter"
variant="danger"
>
hooks.grade-filter-text
</Alert>
</Fragment>
`;

View File

@@ -1,5 +1,6 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { actions, selectors } from 'data/redux/hooks';
import useStatusAlertsData from './hooks';
@@ -15,18 +16,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const validity = {
isMinValid: true,
isMaxValid: true,
@@ -60,7 +49,7 @@ describe('useStatusAlertsData', () => {
expect(out.successBanner.show).toEqual(showSuccess);
});
test('message', () => {
expect(out.successBanner.text).toEqual(messages.editSuccessAlert.defaultMessage);
expect(out.successBanner.text).toEqual(formatMessage(messages.editSuccessAlert));
});
});
describe('gradeFilter', () => {
@@ -81,7 +70,7 @@ describe('useStatusAlertsData', () => {
expect(out.gradeFilter.show).toEqual(true);
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(messages.minGradeInvalid.defaultMessage);
expect(out.gradeFilter.text).toEqual(formatMessage(messages.minGradeInvalid));
});
});
describe('max filter is invalid', () => {
@@ -96,7 +85,7 @@ describe('useStatusAlertsData', () => {
expect(out.gradeFilter.show).toEqual(true);
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(messages.maxGradeInvalid.defaultMessage);
expect(out.gradeFilter.text).toEqual(formatMessage(messages.maxGradeInvalid));
});
});
describe('both filters are invalid', () => {
@@ -112,7 +101,7 @@ describe('useStatusAlertsData', () => {
});
test('filter message', () => {
expect(out.gradeFilter.text).toEqual(
`${messages.minGradeInvalid.defaultMessage}${messages.maxGradeInvalid.defaultMessage}`,
`${formatMessage(messages.minGradeInvalid)}${formatMessage(messages.maxGradeInvalid)}`,
);
});
});

View File

@@ -1,5 +1,7 @@
import React from 'react';
import { render, screen } from '@testing-library/react';
import { shallow } from '@edx/react-unit-test-utils';
import { Alert } from '@openedx/paragon';
import useStatusAlertsData from './hooks';
import StatusAlerts from '.';
@@ -9,20 +11,21 @@ jest.mock('./hooks', () => jest.fn());
const hookProps = {
successBanner: {
onClose: jest.fn().mockName('hooks.successBanner.onClose'),
show: true,
show: 'hooks.show-success-banner',
text: 'hooks.success-banner-text',
},
gradeFilter: {
show: true,
show: 'hooks.show-grade-filter',
text: 'hooks.grade-filter-text',
},
};
useStatusAlertsData.mockReturnValue(hookProps);
let el;
describe('StatusAlerts component', () => {
beforeEach(() => {
jest.clearAllMocks();
render(<StatusAlerts />);
el = shallow(<StatusAlerts />);
});
describe('behavior', () => {
it('initializes component hooks', () => {
@@ -30,17 +33,21 @@ describe('StatusAlerts component', () => {
});
});
describe('render', () => {
it('success banner', () => {
const alerts = screen.getAllByRole('alert');
const successAlert = alerts[0];
expect(successAlert).toHaveTextContent(hookProps.successBanner.text);
expect(successAlert).toHaveClass('alert-success');
test('snapshot', () => {
expect(el.snapshot).toMatchSnapshot();
});
it('grade filter banner', () => {
const alerts = screen.getAllByRole('alert');
const gradeFilter = alerts[1];
expect(gradeFilter).toHaveTextContent(hookProps.gradeFilter.text);
expect(gradeFilter).toHaveClass('alert-danger');
test('success banner', () => {
const alert = el.instance.findByType(Alert)[0];
const { props } = alert;
expect(props.onClose).toEqual(hookProps.successBanner.onClose);
expect(props.show).toEqual(hookProps.successBanner.show);
expect(alert.children[0].el).toEqual(hookProps.successBanner.text);
});
test('grade filter banner', () => {
const alert = el.instance.findByType(Alert)[1];
const { props } = alert;
expect(props.show).toEqual(hookProps.gradeFilter.show);
expect(alert.children[0].el).toEqual(hookProps.gradeFilter.text);
});
});
});

View File

@@ -0,0 +1,11 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SpinnerIcon component snapshot - displays spinner overlay with spinner icon 1`] = `
<div
className="spinner-overlay"
>
<Icon
className="fa fa-spinner fa-spin fa-5x color-black"
/>
</div>
`;

View File

@@ -0,0 +1,41 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`GradesView component render snapshot 1`] = `
<Fragment>
<SpinnerIcon />
<InterventionsReport />
<h3
className="step-message-1"
>
filter-step-heading
</h3>
<div
className="d-flex justify-content-between flex-wrap"
>
<FilterMenuToggle />
<SearchControls />
</div>
<FilterBadges
handleClose={[MockFunction hooks.handleFilterBadgeClose]}
/>
<StatusAlerts />
<h3>
gradebook-step-heading
</h3>
<div
className="d-flex justify-content-between align-items-center mb-2"
>
<ScoreViewInput />
<BulkManagementControls />
</div>
<FilteredUsersLabel />
<GradebookTable />
<PageButtons />
<p>
*
test-masters-hint
</p>
<EditModal />
<ImportSuccessToast />
</Fragment>
`;

View File

@@ -1,5 +1,6 @@
import { useIntl } from '@edx/frontend-platform/i18n';
import { formatMessage } from 'testUtils';
import { actions, thunkActions } from 'data/redux/hooks';
import useGradesViewData from './hooks';
@@ -14,18 +15,6 @@ jest.mock('data/redux/hooks', () => ({
},
}));
jest.mock('react', () => ({
...jest.requireActual('react'),
useContext: jest.fn(context => context),
}));
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
useIntl: jest.fn(() => ({
formatMessage: (message) => message.defaultMessage,
})),
}));
const fetchGrades = jest.fn();
thunkActions.grades.useFetchGrades.mockReturnValue(fetchGrades);
const resetFilters = jest.fn();
@@ -50,11 +39,11 @@ describe('useGradesViewData', () => {
});
describe('output', () => {
test('stepHeadings', () => {
expect(out.stepHeadings.filter).toEqual(messages.filterStepHeading.defaultMessage);
expect(out.stepHeadings.gradebook).toEqual(messages.gradebookStepHeading.defaultMessage);
expect(out.stepHeadings.filter).toEqual(formatMessage(messages.filterStepHeading));
expect(out.stepHeadings.gradebook).toEqual(formatMessage(messages.gradebookStepHeading));
});
test('mastersHint', () => {
expect(out.mastersHint).toEqual(messages.mastersHint.defaultMessage);
expect(out.mastersHint).toEqual(formatMessage(messages.mastersHint));
});
describe('handleFilterBadgeClose', () => {
it('resets filters locally and in query params, and fetches grades', () => {

Some files were not shown because too many files have changed in this diff Show More