Compare commits
36 Commits
release/te
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a2c6aae3b | ||
|
|
d4c6d93c26 | ||
|
|
0ff61e11d5 | ||
|
|
d9e5ae0c80 | ||
|
|
3c03358d4e | ||
|
|
f7e6e30d99 | ||
|
|
ae365b6951 | ||
|
|
729cb40c66 | ||
|
|
bc4abcdeef | ||
|
|
508c91d487 | ||
|
|
28634843d0 | ||
|
|
b7b9b9d81d | ||
|
|
bab0962b6d | ||
|
|
c564150cb5 | ||
|
|
f438360fdb | ||
|
|
4ce7209230 | ||
|
|
266589bca6 | ||
|
|
a579455e58 | ||
|
|
b10aa63723 | ||
|
|
377bb6bbc3 | ||
|
|
ac03594943 | ||
|
|
8e7bba5365 | ||
|
|
e4c0b1843d | ||
|
|
480262a7a2 | ||
|
|
66d5b01a6e | ||
|
|
57022ed294 | ||
|
|
3115fc275c | ||
|
|
f49c6a55f2 | ||
|
|
d71edbd2f2 | ||
|
|
715cc60c1c | ||
|
|
ec3c25f54a | ||
|
|
b54e8ffc85 | ||
|
|
5a383479ff | ||
|
|
cc4b1c8169 | ||
|
|
fc7370c593 | ||
|
|
01bc2cb545 |
2
.env
2
.env
@@ -33,3 +33,5 @@ ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -38,3 +38,5 @@ ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -36,3 +36,4 @@ ENTERPRISE_MARKETING_UTM_SOURCE='example.com'
|
||||
ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM='Footer'
|
||||
ACCOUNT_SETTINGS_URL=http://localhost:1997
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
18
.eslintrc.js
18
.eslintrc.js
@@ -7,20 +7,28 @@ const config = createConfig('eslint', {
|
||||
'import/no-named-as-default-member': 'off',
|
||||
'import/no-import-module-exports': 'off',
|
||||
'import/no-self-import': 'off',
|
||||
'spaced-comment': ['error', 'always', { 'block': { 'exceptions': ['*'] } }],
|
||||
'spaced-comment': ['error', 'always', { block: { exceptions: ['*'] } }],
|
||||
'react-hooks/rules-of-hooks': 'off',
|
||||
"react/forbid-prop-types": ["error", { "forbid": ["any", "array"] }], // arguable object proptype is use when I do not care about the shape of the object
|
||||
'react/forbid-prop-types': ['error', { forbid: ['any', 'array'] }], // arguable object proptype is use when I do not care about the shape of the object
|
||||
'no-import-assign': 'off',
|
||||
'no-promise-executor-return': 'off',
|
||||
'import/no-cycle': 'off',
|
||||
},
|
||||
overrides: [
|
||||
{
|
||||
files: ['**/*.test.{js,jsx,ts,tsx}'],
|
||||
rules: {
|
||||
'react/prop-types': 'off',
|
||||
},
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
config.settings = {
|
||||
"import/resolver": {
|
||||
'import/resolver': {
|
||||
node: {
|
||||
paths: ["src", "node_modules"],
|
||||
extensions: [".js", ".jsx"],
|
||||
paths: ['src', 'node_modules'],
|
||||
extensions: ['.js', '.jsx'],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
6
.github/CODEOWNERS
vendored
6
.github/CODEOWNERS
vendored
@@ -1,6 +0,0 @@
|
||||
# Code owners for frontend-app-ora-grading
|
||||
|
||||
# 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.
|
||||
* @edx/content-aurora
|
||||
16
README.rst
16
README.rst
@@ -26,18 +26,14 @@ Getting Started
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
The `devstack`_ is currently recommended as a development environment for your
|
||||
new MFE. If you start it with ``make dev.up.lms`` that should give you
|
||||
everything you need as a companion to this frontend.
|
||||
|
||||
Note that it is also possible to use `Tutor`_ to develop an MFE. You can refer
|
||||
`Tutor`_ is currently recommended as a development environment for your
|
||||
new MFE. Please refer
|
||||
to the `relevant tutor-mfe documentation`_ to get started using it.
|
||||
|
||||
.. _Devstack: https://github.com/openedx/devstack
|
||||
|
||||
.. _Tutor: https://github.com/overhangio/tutor
|
||||
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
|
||||
|
||||
|
||||
Plugins
|
||||
=======
|
||||
@@ -60,9 +56,9 @@ First, clone the repo, install code prerequisites, and start the app.
|
||||
|
||||
``git clone git@github.com:openedx/frontend-app-ora-grading.git``
|
||||
|
||||
2. Use node v18.x.
|
||||
2. Use the version of Node specified in the ``.nvmrc`` file.
|
||||
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
|
||||
Using other major versions of node *may* work, but this is unsupported. For
|
||||
convenience, this repository includes an .nvmrc file to help in setting the
|
||||
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
|
||||
|
||||
7395
package-lock.json
generated
7395
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -29,14 +29,14 @@
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "^1.2.36",
|
||||
"@fortawesome/free-brands-svg-icons": "^5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "^5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/paragon": "^22.16.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@redux-devtools/extension": "3.0.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
@@ -76,9 +76,7 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@openedx/frontend-build": "^14.3.3",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
@@ -89,7 +87,6 @@
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.5"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import NotificationsBanner from 'containers/NotificationsBanner';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import './App.scss';
|
||||
@@ -26,7 +25,6 @@ export const App = ({ courseMetadata, isEnabled }) => (
|
||||
data-testid="header"
|
||||
/>
|
||||
{!isEnabled && <DemoWarning />}
|
||||
<NotificationsBanner />
|
||||
<main data-testid="main">
|
||||
<ListView />
|
||||
</main>
|
||||
|
||||
22
src/App.scss
22
src/App.scss
@@ -1,13 +1,10 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@openedx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.0.0 to work
|
||||
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
|
||||
|
||||
@import "~@edx/frontend-component-footer/dist/_footer";
|
||||
@import "~@edx/frontend-component-header/dist/index";
|
||||
@@ -49,7 +46,22 @@ $input-focus-box-shadow: $input-box-shadow; // hack to get upgrade to paragon 4.
|
||||
right: 1rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-modal .pgn__modal-body {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.pgn__modal-body-content {
|
||||
& img {
|
||||
object-fit: contain;
|
||||
max-width: 100%;
|
||||
height: auto;
|
||||
}
|
||||
|
||||
& blockquote > p {
|
||||
border-left: 2px solid var(--pgn-color-gray-200);
|
||||
margin-left: 1.5rem;
|
||||
padding-left: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
134
src/App.test.jsx
134
src/App.test.jsx
@@ -1,63 +1,101 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import { App } from './App';
|
||||
import { renderWithIntl } from './testUtils';
|
||||
import { App, mapStateToProps } from './App';
|
||||
|
||||
// we want to scope these tests to the App component, so we mock some child components to reduce complexity
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedHttpClient: jest.fn(),
|
||||
getLoginRedirectUrl: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-footer', () => ({
|
||||
FooterSlot: () => <div data-testid="footer">Footer</div>,
|
||||
}));
|
||||
|
||||
jest.mock('containers/ListView', () => function ListView() {
|
||||
return <div data-testid="list-view">List View</div>;
|
||||
});
|
||||
|
||||
jest.mock('containers/DemoWarning', () => function DemoWarning() {
|
||||
return <div role="alert" data-testid="demo-warning">Demo Warning</div>;
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
LearningHeader: ({ courseTitle, courseNumber, courseOrg }) => (
|
||||
<div data-testid="header">
|
||||
Header - {courseTitle} {courseNumber} {courseOrg}
|
||||
</div>
|
||||
),
|
||||
}));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
app: {
|
||||
selectors: {
|
||||
courseMetadata: (state) => ({ courseMetadata: state }),
|
||||
isEnabled: (state) => ({ isEnabled: state }),
|
||||
selectors: {
|
||||
app: {
|
||||
courseMetadata: jest.fn((state) => state.courseMetadata || {
|
||||
org: 'test-org',
|
||||
number: 'test-101',
|
||||
title: 'Test Course',
|
||||
}),
|
||||
isEnabled: jest.fn((state) => (state.isEnabled !== undefined ? state.isEnabled : true)),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
LearningHeader: 'Header',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => ({ FooterSlot: 'FooterSlot' }));
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
jest.mock('components/Head', () => 'Head');
|
||||
|
||||
let el;
|
||||
|
||||
describe('App router component', () => {
|
||||
const props = {
|
||||
describe('App component', () => {
|
||||
const defaultProps = {
|
||||
courseMetadata: {
|
||||
org: 'course-org',
|
||||
number: 'course-number',
|
||||
title: 'course-title',
|
||||
org: 'test-org',
|
||||
number: 'test-101',
|
||||
title: 'Test Course',
|
||||
},
|
||||
isEnabled: true,
|
||||
};
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App {...props} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: disabled (show demo warning)', () => {
|
||||
expect(shallow(<App {...props} isEnabled={false} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<App {...props} />);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(el.instance.findByTestId('main')[0].children).toHaveLength(1);
|
||||
expect(el.instance.findByTestId('main')[0].children[0].type).toBe('ListView');
|
||||
});
|
||||
});
|
||||
|
||||
test('Header to use courseMetadata props', () => {
|
||||
const {
|
||||
courseTitle,
|
||||
courseNumber,
|
||||
courseOrg,
|
||||
} = el.instance.findByTestId('header')[0].props;
|
||||
expect(courseTitle).toEqual(props.courseMetadata.title);
|
||||
expect(courseNumber).toEqual(props.courseMetadata.number);
|
||||
expect(courseOrg).toEqual(props.courseMetadata.org);
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders header with course metadata', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
const org = screen.getByText((text) => text.includes('test-org'));
|
||||
expect(org).toBeInTheDocument();
|
||||
const title = screen.getByText((content) => content.includes('Test Course'));
|
||||
expect(title).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders main content', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
|
||||
const main = screen.getByTestId('main');
|
||||
expect(main).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render demo warning when enabled', () => {
|
||||
renderWithIntl(<App {...defaultProps} />);
|
||||
|
||||
const demoWarning = screen.queryByRole('alert');
|
||||
expect(demoWarning).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders demo warning when disabled', () => {
|
||||
renderWithIntl(<App {...defaultProps} isEnabled={false} />);
|
||||
|
||||
const demoWarning = screen.getByRole('alert');
|
||||
expect(demoWarning).toBeInTheDocument();
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
it('maps state properties correctly', () => {
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const mapped = mapStateToProps(testState);
|
||||
|
||||
expect(selectors.app.courseMetadata).toHaveBeenCalledWith(testState);
|
||||
expect(selectors.app.isEnabled).toHaveBeenCalledWith(testState);
|
||||
expect(mapped.courseMetadata).toEqual(selectors.app.courseMetadata(testState));
|
||||
expect(mapped.isEnabled).toEqual(selectors.app.isEnabled(testState));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,44 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`App router component snapshot: disabled (show demo warning) 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
data-testid="header"
|
||||
/>
|
||||
<DemoWarning />
|
||||
<NotificationsBanner />
|
||||
<main
|
||||
data-testid="main"
|
||||
>
|
||||
<ListView />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
data-testid="header"
|
||||
/>
|
||||
<NotificationsBanner />
|
||||
<main
|
||||
data-testid="main"
|
||||
>
|
||||
<ListView />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
@@ -1,28 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
wrapWithRouter={false}
|
||||
>
|
||||
<App />
|
||||
</AppProvider>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
@@ -1,5 +1,6 @@
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
describe('ConfirmModal', () => {
|
||||
@@ -12,10 +13,48 @@ describe('ConfirmModal', () => {
|
||||
onCancel: jest.fn().mockName('this.props.onCancel'),
|
||||
onConfirm: jest.fn().mockName('this.props.onConfirm'),
|
||||
};
|
||||
test('snapshot: closed', () => {
|
||||
expect(shallow(<ConfirmModal {...props} />).snapshot).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot: open', () => {
|
||||
expect(shallow(<ConfirmModal {...props} isOpen />).snapshot).toMatchSnapshot();
|
||||
|
||||
it('should not render content when modal is closed', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.queryByText(props.content)).toBeNull();
|
||||
});
|
||||
|
||||
it('should display content when modal is open', () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
expect(screen.getByText(props.content)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should call onCancel when cancel button is clicked', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(props.cancelText));
|
||||
expect(props.onCancel).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should call onConfirm when confirm button is clicked', async () => {
|
||||
render(
|
||||
<IntlProvider locale="en">
|
||||
<ConfirmModal {...props} isOpen />
|
||||
</IntlProvider>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(props.confirmText));
|
||||
expect(props.onConfirm).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoAlert component snapshot 1`] = `
|
||||
<AlertModal
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction props.onClose]}
|
||||
variant="primary"
|
||||
>
|
||||
Confirm
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction props.onClose]}
|
||||
title="Demo submit prevented"
|
||||
>
|
||||
<p>
|
||||
Grade submission is disabled in the Demo mode of the new ORA Staff Grader.
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
AlertModal,
|
||||
@@ -11,29 +11,30 @@ import {
|
||||
import messages from './messages';
|
||||
|
||||
export const DemoAlert = ({
|
||||
intl: { formatMessage },
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => (
|
||||
<AlertModal
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
return (
|
||||
<AlertModal
|
||||
title={formatMessage(messages.title)}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
footerNode={(
|
||||
<ActionRow>
|
||||
<Button variant="primary" onClick={onClose}>
|
||||
{formatMessage(messages.confirm)}
|
||||
</Button>
|
||||
</ActionRow>
|
||||
)}
|
||||
>
|
||||
<p>{formatMessage(messages.warningMessage)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
>
|
||||
<p>{formatMessage(messages.warningMessage)}</p>
|
||||
</AlertModal>
|
||||
);
|
||||
};
|
||||
DemoAlert.propTypes = {
|
||||
intl: intlShape.isRequired,
|
||||
isOpen: PropTypes.bool.isRequired,
|
||||
onClose: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(DemoAlert);
|
||||
export default DemoAlert;
|
||||
|
||||
@@ -1,16 +1,32 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import messages from './messages';
|
||||
import { DemoAlert } from '.';
|
||||
|
||||
describe('DemoAlert component', () => {
|
||||
test('snapshot', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
expect(shallow(<DemoAlert {...props} />).snapshot).toMatchSnapshot();
|
||||
const props = {
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
|
||||
it('does not render when isOpen is false', () => {
|
||||
renderWithIntl(<DemoAlert {...props} isOpen={false} />);
|
||||
expect(screen.queryByText(messages.title.defaultMessage)).toBeNull();
|
||||
});
|
||||
|
||||
it('renders with correct title and message when isOpen is true', () => {
|
||||
renderWithIntl(<DemoAlert {...props} />);
|
||||
expect(screen.getByText(messages.title.defaultMessage)).toBeInTheDocument();
|
||||
expect(screen.getByText(messages.warningMessage.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClose when confirmation button is clicked', async () => {
|
||||
renderWithIntl(<DemoAlert {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const confirmButton = screen.getByText(messages.confirm.defaultMessage);
|
||||
await user.click(confirmButton);
|
||||
expect(props.onClose).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,89 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePopoverContent component snapshot default 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
some file name
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Description"
|
||||
description="Popover title for file description"
|
||||
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
filesize(6000)
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
|
||||
exports[`FilePopoverContent component snapshot invalid size 1`] = `
|
||||
<Fragment>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Name"
|
||||
description="Popover title for file name"
|
||||
id="ora-grading.FilePopoverContent.filePopoverNameTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
some file name
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Description"
|
||||
description="Popover title for file description"
|
||||
id="ora-grading.FilePopoverCellContent.filePopoverDescriptionTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
long descriptive text...
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
defaultMessage="File Size"
|
||||
description="Popover title for file size"
|
||||
id="ora-grading.FilePopoverCellContent.fileSizeTitle"
|
||||
/>
|
||||
</strong>
|
||||
<br />
|
||||
Unknown
|
||||
</div>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import filesize from 'filesize';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import FilePopoverContent from '.';
|
||||
|
||||
jest.mock('filesize', () => (size) => `filesize(${size})`);
|
||||
@@ -14,24 +14,26 @@ describe('FilePopoverContent', () => {
|
||||
downloadURL: 'this-url-is.working',
|
||||
size: 6000,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverContent {...props} />);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('default', () => expect(el.snapshot).toMatchSnapshot());
|
||||
test('invalid size', () => {
|
||||
el = shallow(<FilePopoverContent {...props} size={null} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
const childElements = el.instance.children;
|
||||
expect(childElements[0].children[2].el).toContain(props.name);
|
||||
expect(childElements[1].children[2].el).toContain(props.description);
|
||||
expect(childElements[2].children[2].el).toContain(filesize(props.size));
|
||||
it('renders file name correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(props.name)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders file description correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(props.description)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders file size correctly', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} />);
|
||||
expect(screen.getByText(filesize(props.size))).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders "Unknown" when size is null', () => {
|
||||
renderWithIntl(<FilePopoverContent {...props} size={null} />);
|
||||
expect(screen.getByText('Unknown')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../../testUtils';
|
||||
import ErrorBanner from './ErrorBanner';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
describe('Error Banner component', () => {
|
||||
@@ -25,39 +23,29 @@ describe('Error Banner component', () => {
|
||||
children,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ErrorBanner {...props} />);
|
||||
});
|
||||
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('children node', () => {
|
||||
const childElement = el.instance.children[1];
|
||||
const child = shallow(children);
|
||||
|
||||
expect(childElement.type).toEqual(child.type);
|
||||
expect(childElement.children[0].el).toEqual(child.children[0].el);
|
||||
describe('behavior', () => {
|
||||
it('renders children content', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const childText = screen.getByText('Abitary Child');
|
||||
expect(childText).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('verify actions', () => {
|
||||
const { actions } = el.instance.findByType('Alert')[0].props;
|
||||
expect(actions).toHaveLength(props.actions.length);
|
||||
|
||||
actions.forEach((action, index) => {
|
||||
expect(action.type).toEqual('Button');
|
||||
expect(action.props.onClick).toEqual(props.actions[index].onClick);
|
||||
// action message
|
||||
expect(action.props.children.props).toEqual(props.actions[index].message);
|
||||
});
|
||||
it('renders the correct number of action buttons', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const buttons = screen.getAllByText(messages.retryButton.defaultMessage);
|
||||
expect(buttons).toHaveLength(2);
|
||||
});
|
||||
|
||||
test('verify heading', () => {
|
||||
const heading = el.instance.findByType('FormattedMessage')[0];
|
||||
expect(heading.props).toEqual(props.headingMessage);
|
||||
it('renders error heading with correct message', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const heading = screen.getAllByText(messages.unknownError.defaultMessage)[0];
|
||||
expect(heading).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders with danger variant', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert-danger');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import LoadingBanner from './LoadingBanner';
|
||||
|
||||
describe('Loading Banner component', () => {
|
||||
test('snapshot', () => {
|
||||
const el = shallow(<LoadingBanner />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
describe('behavior', () => {
|
||||
it('renders an info alert', () => {
|
||||
render(<LoadingBanner />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toHaveClass('alert-info');
|
||||
});
|
||||
|
||||
it('renders a spinner', () => {
|
||||
const { container } = render(<LoadingBanner />);
|
||||
const spinner = container.querySelector('.pgn__spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
expect(spinner).toHaveClass('spinner-border');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,42 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
[
|
||||
<Button
|
||||
onClick={[MockFunction action1.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
<Button
|
||||
onClick={[MockFunction action2.onClick]}
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Retry"
|
||||
description="Retry button for error in file renderer"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.retryButton"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Unknown errors"
|
||||
description="Unknown errors message"
|
||||
id="ora-grading.ResponseDisplay.FileRenderer.unknownError"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
Abitary Child
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,12 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Loading Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
variant="info"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
className="d-flex m-auto"
|
||||
/>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,21 +1,40 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import ImageRenderer from './ImageRenderer';
|
||||
|
||||
describe('Image Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.jpg',
|
||||
fileName: 'test-image.jpg',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
it('renders an image with the correct src and alt attributes', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
const imgElement = screen.getByRole('img');
|
||||
expect(imgElement).toBeInTheDocument();
|
||||
expect(imgElement).toHaveAttribute('src', props.url);
|
||||
expect(imgElement).toHaveAttribute('alt', props.fileName);
|
||||
expect(imgElement).toHaveClass('image-renderer');
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
|
||||
it('calls onSuccess when image loads successfully', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
|
||||
const imgElement = screen.getByRole('img');
|
||||
imgElement.dispatchEvent(new Event('load'));
|
||||
|
||||
expect(props.onSuccess).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('calls onError when image fails to load', () => {
|
||||
render(<ImageRenderer {...props} />);
|
||||
|
||||
const imgElement = screen.getByRole('img');
|
||||
imgElement.dispatchEvent(new Event('error'));
|
||||
|
||||
expect(props.onError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { render } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import PDFRenderer from './PDFRenderer';
|
||||
|
||||
import * as hooks from './pdfHooks';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
Document: jest.fn(),
|
||||
Page: jest.fn(),
|
||||
}));
|
||||
|
||||
Document.mockImplementation((props) => <div data-testid="pdf-document">{props.children}</div>);
|
||||
Document.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
|
||||
Page.mockImplementation(() => <div data-testid="pdf-page">Page Content</div>);
|
||||
|
||||
jest.mock('./pdfHooks', () => ({
|
||||
rendererHooks: jest.fn(),
|
||||
}));
|
||||
@@ -33,25 +39,45 @@ describe('PDF Renderer Component', () => {
|
||||
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
|
||||
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
|
||||
hasNext: true,
|
||||
hasPref: false,
|
||||
hasPrev: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
test('first page, prev is disabled', () => {
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the PDF document with navigation controls', () => {
|
||||
hooks.rendererHooks.mockReturnValue(hookProps);
|
||||
expect(shallow(<PDFRenderer {...props} />).snapshot).toMatchSnapshot();
|
||||
const { getByTestId, getAllByText, container } = render(<PDFRenderer {...props} />);
|
||||
expect(getByTestId('pdf-document')).toBeInTheDocument();
|
||||
expect(getByTestId('pdf-page')).toBeInTheDocument();
|
||||
expect(container.querySelector('input[type="number"]')).toBeInTheDocument();
|
||||
expect(getAllByText(/Page/).length).toBeGreaterThan(0);
|
||||
expect(getAllByText(`of ${hookProps.numPages}`).length).toBeGreaterThan(0);
|
||||
});
|
||||
test('on last page, next is disabled', () => {
|
||||
|
||||
it('should have disabled previous button when on the first page', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
hasPrev: false,
|
||||
});
|
||||
|
||||
const { container } = render(<PDFRenderer {...props} />);
|
||||
const prevButton = container.querySelector('button[aria-label="previous pdf page"]');
|
||||
expect(prevButton).toBeDisabled();
|
||||
});
|
||||
|
||||
it('should have disabled next button when on the last page', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
pageNumber: hookProps.numPages,
|
||||
hasNext: false,
|
||||
hasPrev: true,
|
||||
});
|
||||
expect(shallow(<PDFRenderer {...props} />).snapshot).toMatchSnapshot();
|
||||
|
||||
const { container } = render(<PDFRenderer {...props} />);
|
||||
const nextButton = container.querySelector('button[aria-label="next pdf page"]');
|
||||
expect(nextButton).toBeDisabled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,23 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('./textHooks', () => {
|
||||
const content = 'test-content';
|
||||
const mockRendererHooks = jest.fn().mockReturnValue({ content: 'test-content' });
|
||||
return {
|
||||
content,
|
||||
rendererHooks: (args) => ({ content, rendererHooks: args }),
|
||||
rendererHooks: mockRendererHooks,
|
||||
};
|
||||
});
|
||||
|
||||
const textHooks = require('./textHooks');
|
||||
|
||||
describe('TXT Renderer Component', () => {
|
||||
const props = {
|
||||
url: 'some_url.txt',
|
||||
onError: jest.fn().mockName('this.props.onError'),
|
||||
onSuccess: jest.fn().mockName('this.props.onSuccess'),
|
||||
};
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<TXTRenderer {...props} />).snapshot).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
textHooks.rendererHooks.mockClear();
|
||||
});
|
||||
|
||||
it('renders the text content in a pre element', () => {
|
||||
const { getByText, container } = render(<TXTRenderer {...props} />);
|
||||
expect(getByText('test-content')).toBeInTheDocument();
|
||||
expect(container.querySelector('pre')).toHaveClass('txt-renderer');
|
||||
});
|
||||
|
||||
it('passes the correct props to rendererHooks', () => {
|
||||
render(<TXTRenderer {...props} />);
|
||||
expect(textHooks.rendererHooks).toHaveBeenCalledWith({
|
||||
url: props.url,
|
||||
onError: props.onError,
|
||||
onSuccess: props.onSuccess,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Image Renderer Component snapshot 1`] = `
|
||||
<img
|
||||
alt=""
|
||||
className="image-renderer"
|
||||
onError={[MockFunction this.props.onError]}
|
||||
onLoad={[MockFunction this.props.onSuccess]}
|
||||
src="some_url.jpg"
|
||||
/>
|
||||
`;
|
||||
@@ -1,137 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PDF Renderer Component snapshots first page, prev is disabled 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction hooks.onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
{
|
||||
"height": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
|
||||
pageNumber={1}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow
|
||||
className="d-flex justify-content-center m-0"
|
||||
>
|
||||
<IconButton
|
||||
alt="previous pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onPrevPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronLeft]}
|
||||
/>
|
||||
<Form.Group
|
||||
className="d-flex align-items-center m-0"
|
||||
>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
Page
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
max={10}
|
||||
min={0}
|
||||
onChange={[MockFunction hooks.onInputPageChange]}
|
||||
type="number"
|
||||
value={1}
|
||||
/>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
10
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={false}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`PDF Renderer Component snapshots on last page, next is disabled 1`] = `
|
||||
<div
|
||||
className="pdf-renderer"
|
||||
>
|
||||
<Document
|
||||
file="some_url.pdf"
|
||||
onLoadError={[MockFunction hooks.onDocumentLoadError]}
|
||||
onLoadSuccess={[MockFunction hooks.onDocumentLoadSuccess]}
|
||||
>
|
||||
<div
|
||||
className="page-wrapper"
|
||||
style={
|
||||
{
|
||||
"height": 200,
|
||||
}
|
||||
}
|
||||
>
|
||||
<Page
|
||||
onLoadSuccess={[MockFunction hooks.onLoadPageSuccess]}
|
||||
pageNumber={10}
|
||||
/>
|
||||
</div>
|
||||
</Document>
|
||||
<ActionRow
|
||||
className="d-flex justify-content-center m-0"
|
||||
>
|
||||
<IconButton
|
||||
alt="previous pdf page"
|
||||
disabled={false}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onPrevPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronLeft]}
|
||||
/>
|
||||
<Form.Group
|
||||
className="d-flex align-items-center m-0"
|
||||
>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
Page
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
max={10}
|
||||
min={0}
|
||||
onChange={[MockFunction hooks.onInputPageChange]}
|
||||
type="number"
|
||||
value={10}
|
||||
/>
|
||||
<Form.Label
|
||||
isInline={true}
|
||||
>
|
||||
of
|
||||
10
|
||||
</Form.Label>
|
||||
</Form.Group>
|
||||
<IconButton
|
||||
alt="next pdf page"
|
||||
disabled={true}
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction hooks.onNextPageButtonClick]}
|
||||
size="inline"
|
||||
src={[MockFunction icons.ChevronRight]}
|
||||
/>
|
||||
</ActionRow>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
test-content
|
||||
</pre>
|
||||
`;
|
||||
@@ -12,6 +12,11 @@ jest.mock('react-pdf', () => ({
|
||||
Page: () => 'Page',
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useRef: jest.fn((val) => ({ current: val, useRef: true })),
|
||||
}));
|
||||
|
||||
const state = new MockUseState(hooks);
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ export const fetchFile = async ({
|
||||
onSuccess();
|
||||
setContent(data);
|
||||
})
|
||||
.catch((e) => onError(e.response.status));
|
||||
.catch((e) => onError(e.response?.status));
|
||||
|
||||
export const rendererHooks = ({ url, onError, onSuccess }) => {
|
||||
const [content, setContent] = module.state.content('');
|
||||
|
||||
@@ -10,6 +10,11 @@ jest.mock('axios', () => ({
|
||||
get: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: jest.fn((cb, prereqs) => ({ useEffect: { cb, prereqs } })),
|
||||
}));
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
const state = new MockUseState(hooks);
|
||||
|
||||
|
||||
@@ -1,7 +1,5 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
.file-card {
|
||||
margin: map-get($spacers, 1) 0;
|
||||
margin: var(--pgn-spacing-spacer-1) 0;
|
||||
|
||||
.file-card-title {
|
||||
text-overflow: ellipsis;
|
||||
@@ -26,8 +24,8 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
.file-card-title {
|
||||
width: calc(map-get($container-max-widths, "sm")/2);
|
||||
width: calc(var(--pgn-size-container-max-width-sm)/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,4 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
@@ -19,24 +13,27 @@ describe('File Preview Card component', () => {
|
||||
},
|
||||
};
|
||||
const children = (<h1>some children</h1>);
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileCard {...props}>{children}</FileCard>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('collapsible title is name header', () => {
|
||||
const { title } = el.instance.findByType(Collapsible)[0].props;
|
||||
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
|
||||
it('renders with the file name in the title', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
expect(screen.getByText(props.file.name)).toBeInTheDocument();
|
||||
expect(screen.getByText(props.file.name)).toHaveClass('file-card-title');
|
||||
});
|
||||
test('forwards children into preview-panel', () => {
|
||||
const previewPanelChildren = el.instance.findByTestId('preview-panel')[0].children;
|
||||
expect(previewPanelChildren[0].matches(
|
||||
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>,
|
||||
));
|
||||
expect(previewPanelChildren[1].matches(shallow(children))).toEqual(true);
|
||||
|
||||
it('renders the preview panel with file info', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
const previewPanel = screen.getByTestId('preview-panel');
|
||||
expect(previewPanel).toBeInTheDocument();
|
||||
expect(document.querySelector('FileInfo')).toBeInTheDocument();
|
||||
expect(document.querySelector('FilePopoverContent')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders children in the preview panel', () => {
|
||||
render(<FileCard {...props}>{children}</FileCard>);
|
||||
const previewPanel = screen.getByTestId('preview-panel');
|
||||
expect(previewPanel).toBeInTheDocument();
|
||||
expect(screen.getByText('some children')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Popover } from '@openedx/paragon';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import FileInfo from './FileInfo';
|
||||
import messages from './messages';
|
||||
|
||||
describe('File Preview Card component', () => {
|
||||
describe('FileInfo component', () => {
|
||||
const children = (<h1>some Children</h1>);
|
||||
const props = { onClick: jest.fn().mockName('this.props.onClick') };
|
||||
let el;
|
||||
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileInfo {...props}>{children}</FileInfo>);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
test('overlay with passed children', () => {
|
||||
const { overlay } = el.instance.props;
|
||||
expect(overlay.type).toEqual(Popover);
|
||||
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>);
|
||||
|
||||
describe('Component rendering', () => {
|
||||
it('renders the FileInfo button with correct text', () => {
|
||||
renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
|
||||
expect(screen.getByText(messages.fileInfo.defaultMessage)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when button is clicked', async () => {
|
||||
renderWithIntl(<FileInfo {...props}>{children}</FileInfo>);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByText(messages.fileInfo.defaultMessage));
|
||||
expect(props.onClick).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import FileCard from './FileCard';
|
||||
import { ErrorBanner, LoadingBanner } from './Banners';
|
||||
@@ -12,8 +12,8 @@ import { renderHooks } from './hooks';
|
||||
*/
|
||||
export const FileRenderer = ({
|
||||
file,
|
||||
intl,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const {
|
||||
Renderer,
|
||||
isLoading,
|
||||
@@ -39,8 +39,6 @@ FileRenderer.propTypes = {
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(FileRenderer);
|
||||
export default FileRenderer;
|
||||
|
||||
@@ -1,53 +1,79 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { keyStore } from 'utils';
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { FileRenderer } from './FileRenderer';
|
||||
import * as hooks from './hooks';
|
||||
|
||||
jest.mock('./FileCard', () => 'FileCard');
|
||||
jest.mock('./Banners', () => ({
|
||||
ErrorBanner: () => 'ErrorBanner',
|
||||
LoadingBanner: () => 'LoadingBanner',
|
||||
}));
|
||||
|
||||
const hookKeys = keyStore(hooks);
|
||||
|
||||
const props = {
|
||||
file: {
|
||||
downloadUrl: 'file download url',
|
||||
name: 'filename.txt',
|
||||
description: 'A text file',
|
||||
},
|
||||
intl: { formatMessage },
|
||||
};
|
||||
describe('FileRenderer', () => {
|
||||
describe('component', () => {
|
||||
describe('snapshot', () => {
|
||||
test('isLoading, no Error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => 'Renderer',
|
||||
isloading: true,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
expect(shallow(<FileRenderer {...props} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('is not loading, with error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => 'Renderer',
|
||||
isloading: false,
|
||||
errorStatus: ErrorStatuses.serverError,
|
||||
error: { prop: 'hooks.errorProps' },
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
expect(shallow(<FileRenderer {...props} />).snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('renders loading banner when isLoading is true', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: true,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
|
||||
const spinner = document.querySelector('.spinner-border');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
it('renders error banner when there is an error status', () => {
|
||||
const errorProps = {
|
||||
headingMessage: { id: 'error.heading', defaultMessage: 'Error Heading' },
|
||||
children: 'Error Message',
|
||||
actions: [{ id: 'retry', onClick: jest.fn(), message: { id: 'retry', defaultMessage: 'Retry' } }],
|
||||
};
|
||||
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: false,
|
||||
errorStatus: ErrorStatuses.serverError,
|
||||
error: errorProps,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByText('Error Message')).toBeInTheDocument();
|
||||
expect(document.querySelector('.alert-heading')).toBeInTheDocument();
|
||||
expect(document.querySelector('.btn.btn-outline-primary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders renderer component when not loading and no error', () => {
|
||||
const hookProps = {
|
||||
Renderer: () => <div data-testid="mock-renderer">Renderer Component</div>,
|
||||
isLoading: false,
|
||||
errorStatus: null,
|
||||
error: null,
|
||||
rendererProps: { prop: 'hooks.rendererProps' },
|
||||
};
|
||||
jest.spyOn(hooks, hookKeys.renderHooks).mockReturnValueOnce(hookProps);
|
||||
|
||||
renderWithIntl(<FileRenderer {...props} />);
|
||||
|
||||
expect(screen.getByText('filename.txt')).toBeInTheDocument();
|
||||
expect(screen.getByTestId('mock-renderer')).toBeInTheDocument();
|
||||
expect(screen.getByText('Renderer Component')).toBeInTheDocument();
|
||||
|
||||
const spinner = document.querySelector('.spinner-border');
|
||||
expect(spinner).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,36 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<Card
|
||||
className="file-card"
|
||||
key="test-file-name.pdf"
|
||||
>
|
||||
<Collapsible
|
||||
className="file-collapsible"
|
||||
defaultOpen={true}
|
||||
title={
|
||||
<h3
|
||||
className="file-card-title"
|
||||
>
|
||||
test-file-name.pdf
|
||||
</h3>
|
||||
}
|
||||
>
|
||||
<div
|
||||
className="preview-panel"
|
||||
data-testid="preview-panel"
|
||||
>
|
||||
<FileInfo>
|
||||
<FilePopoverContent
|
||||
description="test-file description"
|
||||
downloadUrl="destination/test-file-name.pdf"
|
||||
name="test-file-name.pdf"
|
||||
/>
|
||||
</FileInfo>
|
||||
<h1>
|
||||
some children
|
||||
</h1>
|
||||
</div>
|
||||
</Collapsible>
|
||||
</Card>
|
||||
`;
|
||||
@@ -1,34 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`File Preview Card component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
id="file-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<h1>
|
||||
some Children
|
||||
</h1>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
}
|
||||
placement="right-end"
|
||||
trigger="focus"
|
||||
>
|
||||
<Button
|
||||
iconAfter={[MockFunction icons.InfoOutline]}
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
size="sm"
|
||||
variant="tertiary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="File info"
|
||||
description="Popover trigger button text for file preview card"
|
||||
id="ora-grading.InfoPopover.fileInfo"
|
||||
/>
|
||||
</Button>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -1,33 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
{
|
||||
"downloadUrl": "file download url",
|
||||
"name": "filename.txt",
|
||||
}
|
||||
}
|
||||
key="file download url"
|
||||
>
|
||||
<ErrorBanner
|
||||
prop="hooks.errorProps"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
|
||||
exports[`FileRenderer component snapshot isLoading, no Error 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
{
|
||||
"downloadUrl": "file download url",
|
||||
"name": "filename.txt",
|
||||
}
|
||||
}
|
||||
key="file download url"
|
||||
>
|
||||
<Renderer
|
||||
prop="hooks.rendererProps"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
@@ -1,14 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Head snapshot 1`] = `
|
||||
<Helmet>
|
||||
<title>
|
||||
ORA staff grading | site-name
|
||||
</title>
|
||||
<link
|
||||
href="favicon-url"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</Helmet>
|
||||
`;
|
||||
@@ -1,25 +1,45 @@
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { render } from '@testing-library/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import Head from '.';
|
||||
|
||||
jest.mock('react-helmet', () => ({
|
||||
Helmet: 'Helmet',
|
||||
jest.mock('@edx/frontend-platform/i18n', () => ({
|
||||
useIntl: () => ({
|
||||
formatMessage: (message, values) => {
|
||||
if (message.defaultMessage && values) {
|
||||
return message.defaultMessage.replace('{siteName}', values.siteName);
|
||||
}
|
||||
return message.defaultMessage || message.id;
|
||||
},
|
||||
}),
|
||||
defineMessages: (messages) => messages,
|
||||
}));
|
||||
|
||||
jest.mock('react-helmet', () => ({
|
||||
Helmet: jest.fn(),
|
||||
}));
|
||||
|
||||
Helmet.mockImplementation(({ children }) => <div data-testid="helmet-mock">{children}</div>);
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: () => ({
|
||||
getConfig: jest.fn().mockReturnValue({
|
||||
SITE_NAME: 'site-name',
|
||||
FAVICON_URL: 'favicon-url',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Head', () => {
|
||||
it('snapshot', () => {
|
||||
const el = shallow(<Head />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
it('should render page title with site name from config', () => {
|
||||
const { container } = render(<Head />);
|
||||
const titleElement = container.querySelector('title');
|
||||
expect(titleElement).toBeInTheDocument();
|
||||
expect(titleElement.textContent).toContain('ORA staff grading | site-name');
|
||||
});
|
||||
|
||||
expect(el.instance.findByType('title')[0].el.children[0]).toContain(getConfig().SITE_NAME);
|
||||
expect(el.instance.findByType('link')[0].props.href).toEqual(getConfig().FAVICON_URL);
|
||||
it('should render favicon link with URL from config', () => {
|
||||
const { container } = render(<Head />);
|
||||
const faviconLink = container.querySelector('link[rel="shortcut icon"]');
|
||||
expect(faviconLink).toBeInTheDocument();
|
||||
expect(faviconLink.getAttribute('href')).toEqual('favicon-url');
|
||||
expect(faviconLink.getAttribute('type')).toEqual('image/x-icon');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,30 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Info Popover Component snapshot 1`] = `
|
||||
<OverlayTrigger
|
||||
flip={true}
|
||||
overlay={
|
||||
<Popover
|
||||
className="overlay-help-popover"
|
||||
id="info-popover"
|
||||
>
|
||||
<Popover.Content>
|
||||
<div>
|
||||
Children component
|
||||
</div>
|
||||
</Popover.Content>
|
||||
</Popover>
|
||||
}
|
||||
placement="right-end"
|
||||
trigger="focus"
|
||||
>
|
||||
<IconButton
|
||||
alt="Display more info"
|
||||
className="esg-help-icon"
|
||||
data-testid="esg-help-icon"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
src={[MockFunction icons.InfoOutline]}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
IconButton,
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
@@ -17,27 +17,35 @@ import messages from './messages';
|
||||
/**
|
||||
* <InfoPopover />
|
||||
*/
|
||||
export const InfoPopover = ({ onClick, children, intl }) => (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="right-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover id="info-popover" className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="esg-help-icon"
|
||||
data-testid="esg-help-icon"
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
export const InfoPopover = (
|
||||
{
|
||||
onClick,
|
||||
children,
|
||||
},
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<OverlayTrigger
|
||||
trigger="focus"
|
||||
placement="left-end"
|
||||
flip
|
||||
overlay={(
|
||||
<Popover id="info-popover" className="overlay-help-popover">
|
||||
<Popover.Content>{children}</Popover.Content>
|
||||
</Popover>
|
||||
)}
|
||||
>
|
||||
<IconButton
|
||||
className="esg-help-icon"
|
||||
data-testid="esg-help-icon"
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
};
|
||||
|
||||
InfoPopover.defaultProps = {
|
||||
onClick: nullMethod,
|
||||
@@ -48,7 +56,6 @@ InfoPopover.propTypes = {
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(InfoPopover);
|
||||
export default InfoPopover;
|
||||
|
||||
@@ -1,23 +1,31 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { InfoPopover } from '.';
|
||||
|
||||
describe('Info Popover Component', () => {
|
||||
const child = <div>Children component</div>;
|
||||
const onClick = jest.fn().mockName('this.props.onClick');
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<InfoPopover onClick={onClick} intl={{ formatMessage }}>{child}</InfoPopover>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('Component', () => {
|
||||
test('Test component render', () => {
|
||||
expect(el.instance.children.length).toEqual(1);
|
||||
expect(el.instance.findByTestId('esg-help-icon').length).toEqual(1);
|
||||
it('renders the help icon button', () => {
|
||||
renderWithIntl(
|
||||
<InfoPopover onClick={onClick}>
|
||||
{child}
|
||||
</InfoPopover>,
|
||||
);
|
||||
expect(screen.getByTestId('esg-help-icon')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls onClick when the help icon is clicked', async () => {
|
||||
renderWithIntl(
|
||||
<InfoPopover onClick={onClick}>
|
||||
{child}
|
||||
</InfoPopover>,
|
||||
);
|
||||
const user = userEvent.setup();
|
||||
await user.click(screen.getByTestId('esg-help-icon'));
|
||||
expect(onClick).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,34 +1,36 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { gradingStatuses } from 'data/services/lms/constants';
|
||||
import messages from '../data/services/lms/messages';
|
||||
import { renderWithIntl } from '../testUtils';
|
||||
import { StatusBadge } from './StatusBadge';
|
||||
|
||||
const className = 'test-className';
|
||||
describe('StatusBadge component', () => {
|
||||
const render = (status) => shallow(<StatusBadge className={className} status={status} />);
|
||||
describe('behavior', () => {
|
||||
it('does not render if status does not have configured variant', () => {
|
||||
const el = render('arbitrary');
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
const { container } = renderWithIntl(<StatusBadge className={className} status="arbitrary" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
describe('status snapshots: loads badge with configured variant and message.', () => {
|
||||
test('`ungraded` shows primary button variant and message', () => {
|
||||
const el = render(gradingStatuses.ungraded);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
describe('status rendering: loads badge with configured variant and message', () => {
|
||||
it('`ungraded` shows primary button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.ungraded} />);
|
||||
const badge = screen.getByText(messages.ungraded.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-primary');
|
||||
});
|
||||
test('`locked` shows light button variant and message', () => {
|
||||
const el = render(gradingStatuses.locked);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
it('`locked` shows light button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.locked} />);
|
||||
const badge = screen.getByText(messages.locked.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-light');
|
||||
});
|
||||
test('`graded` shows success button variant and message', () => {
|
||||
const el = render(gradingStatuses.graded);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
it('`graded` shows success button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.graded} />);
|
||||
const badge = screen.getByText(messages.graded.defaultMessage);
|
||||
expect(badge).toHaveClass('badge-success');
|
||||
});
|
||||
test('`inProgress` shows warning button variant and message', () => {
|
||||
const el = render(gradingStatuses.inProgress);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
it('`inProgress` shows warning button variant and message', () => {
|
||||
renderWithIntl(<StatusBadge className={className} status={gradingStatuses.inProgress} />);
|
||||
const badge = screen.getByText(messages['in-progress'].defaultMessage);
|
||||
expect(badge).toHaveClass('badge-warning');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ConfirmModal snapshot: closed 1`] = `
|
||||
<AlertModal
|
||||
className="confirm-modal"
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onCancel]}
|
||||
variant="tertiary"
|
||||
>
|
||||
test-cancel-text
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onConfirm]}
|
||||
variant="primary"
|
||||
>
|
||||
test-confirm-text
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={false}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
test-content
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
|
||||
exports[`ConfirmModal snapshot: open 1`] = `
|
||||
<AlertModal
|
||||
className="confirm-modal"
|
||||
footerNode={
|
||||
<ActionRow>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onCancel]}
|
||||
variant="tertiary"
|
||||
>
|
||||
test-cancel-text
|
||||
</Button>
|
||||
<Button
|
||||
onClick={[MockFunction this.props.onConfirm]}
|
||||
variant="primary"
|
||||
>
|
||||
test-confirm-text
|
||||
</Button>
|
||||
</ActionRow>
|
||||
}
|
||||
isOpen={true}
|
||||
onClose={[MockFunction hooks.nullMethod]}
|
||||
title="test-title"
|
||||
>
|
||||
<p>
|
||||
test-content
|
||||
</p>
|
||||
</AlertModal>
|
||||
`;
|
||||
@@ -1,55 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusBadge component behavior does not render if status does not have configured variant 1`] = `null`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`graded\` shows success button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="success"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Grading Completed"
|
||||
description="Grading status label for graded submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.graded"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`inProgress\` shows warning button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="warning"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are currently grading this response"
|
||||
description="Grading status label for in-progress submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.inProgress"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`locked\` shows light button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="light"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Currently being graded by someone else"
|
||||
description="Grading status label for locked submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.locked"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
|
||||
exports[`StatusBadge component behavior status snapshots: loads badge with configured variant and message. \`ungraded\` shows primary button variant and message 1`] = `
|
||||
<Badge
|
||||
className="test-className"
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Ungraded"
|
||||
description="Grading status label for ungraded submission"
|
||||
id="ora-grading.lms-api.gradingStatusDisplay.ungraded"
|
||||
/>
|
||||
</Badge>
|
||||
`;
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { feedbackRequirement } from 'data/services/lms/constants';
|
||||
import { actions, selectors } from 'data/redux';
|
||||
@@ -12,60 +12,56 @@ import messages from './messages';
|
||||
/**
|
||||
* <CriterionFeedback />
|
||||
*/
|
||||
export class CriterionFeedback extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
export const CriterionFeedback = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
setValue,
|
||||
value,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
onChange(event) {
|
||||
this.props.setValue({
|
||||
const onChange = (event) => {
|
||||
setValue({
|
||||
value: event.target.value,
|
||||
orderNum: this.props.orderNum,
|
||||
orderNum,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
get commentMessage() {
|
||||
const { config, isGrading } = this.props;
|
||||
let commentMessage = this.translate(isGrading ? messages.addComments : messages.comments);
|
||||
const translate = (msg) => intl.formatMessage(msg);
|
||||
|
||||
const getCommentMessage = () => {
|
||||
let commentMessage = translate(isGrading ? messages.addComments : messages.comments);
|
||||
if (config === feedbackRequirement.optional) {
|
||||
commentMessage += ` ${this.translate(messages.optional)}`;
|
||||
commentMessage += ` ${translate(messages.optional)}`;
|
||||
}
|
||||
return commentMessage;
|
||||
};
|
||||
|
||||
if (config === feedbackRequirement.disabled) {
|
||||
return null;
|
||||
}
|
||||
|
||||
translate = (msg) => this.props.intl.formatMessage(msg);
|
||||
|
||||
render() {
|
||||
const {
|
||||
config,
|
||||
isGrading,
|
||||
value,
|
||||
isInvalid,
|
||||
} = this.props;
|
||||
if (config === feedbackRequirement.disabled) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<Form.Group isInvalid={this.feedbackIsInvalid}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
floatingLabel={this.commentMessage}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg" data-testid="criterion-feedback-error-msg">
|
||||
{this.translate(messages.criterionFeedbackError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Form.Group isInvalid={isInvalid}>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
floatingLabel={getCommentMessage()}
|
||||
value={value}
|
||||
onChange={onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg" data-testid="criterion-feedback-error-msg">
|
||||
{translate(messages.criterionFeedbackError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
CriterionFeedback.defaultProps = {
|
||||
value: '',
|
||||
@@ -74,8 +70,6 @@ CriterionFeedback.defaultProps = {
|
||||
CriterionFeedback.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
config: PropTypes.string.isRequired,
|
||||
setValue: PropTypes.func.isRequired,
|
||||
@@ -93,6 +87,4 @@ export const mapDispatchToProps = {
|
||||
setValue: actions.grading.setCriterionFeedback,
|
||||
};
|
||||
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback),
|
||||
);
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import {
|
||||
feedbackRequirement,
|
||||
gradeStatuses,
|
||||
} from 'data/services/lms/constants';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
CriterionFeedback,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './CriterionFeedback';
|
||||
import messages from './messages';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -36,7 +36,6 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Criterion Feedback', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
config: 'config string',
|
||||
isGrading: true,
|
||||
@@ -45,110 +44,50 @@ describe('Criterion Feedback', () => {
|
||||
setValue: jest.fn().mockName('this.props.setValue'),
|
||||
isInvalid: false,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<CriterionFeedback {...props} />);
|
||||
el.instance.onChange = jest.fn().mockName('this.onChange');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('is grading', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('is graded', () => {
|
||||
el = shallow(<CriterionFeedback {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('feedback value is invalid', () => {
|
||||
el = shallow(<CriterionFeedback {...props} isInvalid />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
Object.values(feedbackRequirement).forEach((requirement) => {
|
||||
test(`feedback is configured to ${requirement}`, () => {
|
||||
el = shallow(<CriterionFeedback {...props} config={requirement} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('render', () => {
|
||||
test('is grading (the feedback input is not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const controlEl = el.instance.findByTestId('criterion-feedback-input')[0];
|
||||
expect(controlEl.props.disabled).toEqual(false);
|
||||
expect(controlEl.props.value).toEqual(props.value);
|
||||
it('shows a non-disabled input when grading', () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} />);
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).not.toBeDisabled();
|
||||
expect(input).toHaveValue(props.value);
|
||||
});
|
||||
test('is graded (the input is disabled)', () => {
|
||||
el = shallow(<CriterionFeedback {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
const controlEl = el.instance.findByTestId('criterion-feedback-input')[0];
|
||||
expect(controlEl.props.disabled).toEqual(true);
|
||||
expect(controlEl.props.value).toEqual(props.value);
|
||||
|
||||
it('shows a disabled input when not grading', () => {
|
||||
renderWithIntl(
|
||||
<CriterionFeedback {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />,
|
||||
);
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input).toBeDisabled();
|
||||
expect(input).toHaveValue(props.value);
|
||||
});
|
||||
test('is having invalid feedback (feedback get render)', () => {
|
||||
el = shallow(<CriterionFeedback {...props} isInvalid />);
|
||||
const feedbackErrorEl = el.instance.findByTestId('criterion-feedback-error-msg');
|
||||
expect(feedbackErrorEl).toBeDefined();
|
||||
|
||||
it('displays an error message when feedback is invalid', () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} isInvalid />);
|
||||
expect(screen.getByTestId('criterion-feedback-error-msg')).toBeInTheDocument();
|
||||
});
|
||||
test('is configure to disabled (the input does not get render)', () => {
|
||||
el = shallow(<CriterionFeedback {...props} config={feedbackRequirement.disabled} />);
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
|
||||
it('does not render anything when config is set to disabled', () => {
|
||||
const { container } = renderWithIntl(
|
||||
<CriterionFeedback {...props} config={feedbackRequirement.disabled} />,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('onChange call set value', () => {
|
||||
el = shallow(<CriterionFeedback {...props} />);
|
||||
el.instance.findByTestId('criterion-feedback-input')[0].props.onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
it('calls setValue when input value changes', async () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const input = screen.getByTestId('criterion-feedback-input');
|
||||
await user.clear(input);
|
||||
expect(props.setValue).toHaveBeenCalledWith({
|
||||
value: '',
|
||||
orderNum: props.orderNum,
|
||||
});
|
||||
expect(props.setValue).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getter commentMessage', () => {
|
||||
test('is grading', () => {
|
||||
let commentMessage;
|
||||
|
||||
el = shallow(<CriterionFeedback {...props} isGrading config={feedbackRequirement.optional} />);
|
||||
commentMessage = el.instance.findByTestId('criterion-feedback-input')[0].props.floatingLabel;
|
||||
expect(commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el = shallow(<CriterionFeedback {...props} config={feedbackRequirement.required} />);
|
||||
commentMessage = el.instance.findByTestId('criterion-feedback-input')[0].props.floatingLabel;
|
||||
expect(commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(commentMessage).toContain(
|
||||
messages.addComments.defaultMessage,
|
||||
);
|
||||
});
|
||||
|
||||
test('is not grading', () => {
|
||||
let commentMessage;
|
||||
|
||||
el = shallow(<CriterionFeedback {...props} isGrading={false} config={feedbackRequirement.optional} />);
|
||||
commentMessage = el.instance.findByTestId('criterion-feedback-input')[0].props.floatingLabel;
|
||||
expect(commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el = shallow(<CriterionFeedback {...props} isGrading={false} config={feedbackRequirement.required} />);
|
||||
commentMessage = el.instance.findByTestId('criterion-feedback-input')[0].props.floatingLabel;
|
||||
expect(commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(commentMessage).toContain(
|
||||
messages.comments.defaultMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -160,17 +99,17 @@ describe('Criterion Feedback', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
it('gets config from selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionFeedbackConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selector.grading.selected.criterionFeedback', () => {
|
||||
it('gets value from selectors.grading.selected.criterionFeedback', () => {
|
||||
expect(mapped.value).toEqual(
|
||||
selectors.grading.selected.criterionFeedback(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selector.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
it('gets isInvalid from selectors.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionFeedbackIsInvalid(
|
||||
testState,
|
||||
@@ -181,7 +120,7 @@ describe('Criterion Feedback', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
it('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
expect(mapDispatchToProps.setValue).toEqual(
|
||||
actions.grading.setCriterionFeedback,
|
||||
);
|
||||
|
||||
@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
@@ -11,51 +11,46 @@ import messages from './messages';
|
||||
/**
|
||||
* <RadioCriterion />
|
||||
*/
|
||||
export class RadioCriterion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
export const RadioCriterion = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
data,
|
||||
setCriterionOption,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
onChange(event) {
|
||||
this.props.setCriterionOption({
|
||||
orderNum: this.props.orderNum,
|
||||
const onChange = (event) => {
|
||||
setCriterionOption({
|
||||
orderNum,
|
||||
value: event.target.value,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const {
|
||||
config,
|
||||
data,
|
||||
intl,
|
||||
isGrading,
|
||||
isInvalid,
|
||||
} = this.props;
|
||||
return (
|
||||
<Form.RadioSet name={config.name} value={data}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{intl.formatMessage(messages.rubricSelectedError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
);
|
||||
}
|
||||
}
|
||||
return (
|
||||
<Form.RadioSet name={config.name} value={data}>
|
||||
{config.options.map((option) => (
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={onChange}
|
||||
disabled={!isGrading}
|
||||
style={{ flexShrink: 0 }}
|
||||
>
|
||||
{option.label}
|
||||
</Form.Radio>
|
||||
))}
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{intl.formatMessage(messages.rubricSelectedError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.RadioSet>
|
||||
);
|
||||
};
|
||||
|
||||
RadioCriterion.defaultProps = {
|
||||
data: {
|
||||
@@ -67,8 +62,6 @@ RadioCriterion.defaultProps = {
|
||||
RadioCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
prompt: PropTypes.string,
|
||||
@@ -99,4 +92,4 @@ export const mapDispatchToProps = {
|
||||
setCriterionOption: actions.grading.setCriterionOption,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(RadioCriterion));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RadioCriterion);
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
RadioCriterion,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './RadioCriterion';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -31,7 +30,6 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Radio Criterion Container', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
isGrading: true,
|
||||
config: {
|
||||
@@ -55,70 +53,47 @@ describe('Radio Criterion Container', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
data: 'selected radio option',
|
||||
data: 'option name',
|
||||
setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'),
|
||||
isInvalid: false,
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance.onChange = jest.fn().mockName('this.onChange');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('is grading', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
describe('component rendering', () => {
|
||||
it('should render radio buttons that are enabled when in grading mode', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} />);
|
||||
|
||||
test('is not grading', () => {
|
||||
el = shallow(<RadioCriterion {...props} isGrading={false} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
const radioButtons = container.querySelectorAll('input[type="radio"]');
|
||||
expect(radioButtons.length).toEqual(props.config.options.length);
|
||||
|
||||
test('radio contain invalid response', () => {
|
||||
el = shallow(<RadioCriterion {...props} isInvalid />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('rendering', () => {
|
||||
test('is grading (all options are not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.instance.children;
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.props.disabled).toEqual(false));
|
||||
});
|
||||
|
||||
test('is not grading (all options are disabled)', () => {
|
||||
el = shallow(<RadioCriterion {...props} isGrading={false} />);
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.instance.children;
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.props.disabled).toEqual(true));
|
||||
});
|
||||
|
||||
test('radio contain invalid response (error response get render)', () => {
|
||||
el = shallow(<RadioCriterion {...props} isInvalid />);
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const radioErrorEl = el.instance.children[2];
|
||||
expect(radioErrorEl.props.type).toBe('invalid');
|
||||
expect(radioErrorEl.props.className).toBe('feedback-error-msg');
|
||||
expect(radioErrorEl).toBeTruthy();
|
||||
radioButtons.forEach(button => {
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('onChange call set crition option', () => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance.children[0].props.onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
});
|
||||
expect(props.setCriterionOption).toBeCalledTimes(1);
|
||||
it('should render radio buttons that are disabled when not in grading mode', () => {
|
||||
renderWithIntl(<RadioCriterion {...props} isGrading={false} />);
|
||||
|
||||
const radioButtons = screen.queryAllByRole('radio');
|
||||
expect(radioButtons.length).toEqual(props.config.options.length);
|
||||
|
||||
radioButtons.forEach(button => {
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
});
|
||||
|
||||
it('should render an error message when the criterion is invalid', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} isInvalid />);
|
||||
|
||||
const errorMessage = container.querySelector('.feedback-error-msg');
|
||||
expect(errorMessage).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should not render an error message when the criterion is valid', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} />);
|
||||
|
||||
const errorMessage = container.querySelector('.feedback-error-msg');
|
||||
expect(errorMessage).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
@@ -128,18 +103,20 @@ describe('Radio Criterion Container', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('should properly map config from rubric criterion config selector', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.criterionSelectedOption', () => {
|
||||
it('should properly map data from selected criterion option selector', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionSelectedOption(testState, ownProps),
|
||||
);
|
||||
});
|
||||
test('selectors.grading.validation.criterionSelectedOptionIsInvalid', () => {
|
||||
|
||||
it('should properly map isInvalid from criterion validation selector', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionSelectedOptionIsInvalid(testState, ownProps),
|
||||
);
|
||||
@@ -147,7 +124,7 @@ describe('Radio Criterion Container', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
it('should map setCriterionOption action to props', () => {
|
||||
expect(mapDispatchToProps.setCriterionOption).toEqual(
|
||||
actions.grading.setCriterionOption,
|
||||
);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ReviewCriterion, mapStateToProps } from './ReviewCriterion';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -20,7 +20,7 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('Review Crition Container', () => {
|
||||
describe('Review Criterion Container', () => {
|
||||
const props = {
|
||||
orderNum: 1,
|
||||
config: {
|
||||
@@ -50,29 +50,20 @@ describe('Review Crition Container', () => {
|
||||
},
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewCriterion {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('rendering (everything show up)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.instance.findByTestId('criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
const option = props.config.options[i];
|
||||
expect(optionEl.props.key).toEqual(option.name);
|
||||
expect(optionEl.findByTestId('option-label')[0].children[0].el).toEqual(
|
||||
option.label,
|
||||
);
|
||||
expect(optionEl.findByTestId('option-points')[0].children[0].props).toEqual({
|
||||
...messages.optionPoints,
|
||||
values: { points: option.points },
|
||||
});
|
||||
it('renders all criteria options with correct labels and points', () => {
|
||||
renderWithIntl(<ReviewCriterion {...props} />);
|
||||
|
||||
const optionsElements = screen.getAllByTestId('criteria-option');
|
||||
expect(optionsElements.length).toEqual(props.config.options.length);
|
||||
|
||||
props.config.options.forEach((option, index) => {
|
||||
const optionElement = optionsElements[index];
|
||||
const labelElement = optionElement.querySelector('[data-testid="option-label"]');
|
||||
const pointsElement = optionElement.querySelector('[data-testid="option-points"]');
|
||||
|
||||
expect(labelElement.textContent).toEqual(option.label);
|
||||
expect(pointsElement.textContent).toEqual(`${props.config.options[index].points} points`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -81,16 +72,18 @@ describe('Review Crition Container', () => {
|
||||
const testState = { arbitrary: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('should map criterion config from state', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.criterionGradeData', () => {
|
||||
it('should map criterion grade data from state', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionGradeData(testState, ownProps),
|
||||
);
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to disabled 1`] = `null`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to optional 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments (Optional)"
|
||||
onChange={[Function]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback is configured to required 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[Function]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot feedback value is invalid 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[Function]}
|
||||
value="criterion value"
|
||||
/>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
data-testid="criterion-feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
The feedback is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is graded 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
onChange={[Function]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
data-testid="criterion-feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[Function]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -1,121 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Radio Criterion Container snapshot is grading 1`] = `
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
key="option name"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
key="option name 2"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot is not grading 1`] = `
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
key="option name"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
key="option name 2"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
|
||||
exports[`Radio Criterion Container snapshot radio contain invalid response 1`] = `
|
||||
<Form.RadioSet
|
||||
name="random name"
|
||||
value="selected radio option"
|
||||
>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
key="option name"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option align-items-center"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
key="option name 2"
|
||||
onChange={[Function]}
|
||||
style={
|
||||
{
|
||||
"flexShrink": 0,
|
||||
}
|
||||
}
|
||||
value="option name 2"
|
||||
>
|
||||
this label 2
|
||||
</Form.Radio>
|
||||
<Form.Control.Feedback
|
||||
className="feedback-error-msg"
|
||||
type="invalid"
|
||||
>
|
||||
Rubric selection is required
|
||||
</Form.Control.Feedback>
|
||||
</Form.RadioSet>
|
||||
`;
|
||||
@@ -1,66 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Review Crition Container snapshot 1`] = `
|
||||
<div
|
||||
className="review-criterion"
|
||||
>
|
||||
<div
|
||||
className="criteria-option"
|
||||
data-testid="criteria-option"
|
||||
key="option name"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
data-testid="option-label"
|
||||
>
|
||||
this label
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
data-testid="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
{
|
||||
"points": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="criteria-option"
|
||||
data-testid="criteria-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
data-testid="option-label"
|
||||
>
|
||||
this label 2
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
data-testid="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
{
|
||||
"points": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,153 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Criterion Container snapshot is graded and is not grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
data-testid="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
data-testid="rubric-criteria"
|
||||
>
|
||||
<RadioCriterion
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={true}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Container snapshot is ungraded and is not grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Label
|
||||
className="criteria-label"
|
||||
>
|
||||
<span
|
||||
className="criteria-title"
|
||||
>
|
||||
prompt
|
||||
</span>
|
||||
<InfoPopover>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
data-testid="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
data-testid="rubric-criteria"
|
||||
>
|
||||
<ReviewCriterion
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -1,15 +1,50 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { gradeStatuses } from 'data/services/lms/constants';
|
||||
|
||||
import { CriterionContainer, mapStateToProps } from '.';
|
||||
|
||||
jest.mock('components/InfoPopover', () => 'InfoPopover');
|
||||
jest.mock('./RadioCriterion', () => 'RadioCriterion');
|
||||
jest.mock('./CriterionFeedback', () => 'CriterionFeedback');
|
||||
jest.mock('./ReviewCriterion', () => 'ReviewCriterion');
|
||||
const MockRadioCriterion = ({ orderNum, isGrading }) => (
|
||||
<div data-testid="radio-criterion-component">
|
||||
RadioCriterion Component (orderNum={orderNum}, isGrading={String(isGrading)})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockRadioCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const MockReviewCriterion = ({ orderNum }) => (
|
||||
<div data-testid="review-criterion-component">
|
||||
ReviewCriterion Component (orderNum={orderNum})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockReviewCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
const MockCriterionFeedback = ({ orderNum, isGrading }) => (
|
||||
<div data-testid="criterion-feedback-component">
|
||||
CriterionFeedback Component (orderNum={orderNum}, isGrading={String(isGrading)})
|
||||
</div>
|
||||
);
|
||||
|
||||
MockCriterionFeedback.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
};
|
||||
|
||||
const MockInfoPopover = ({ children }) => (
|
||||
<div data-testid="info-popover">{children}</div>
|
||||
);
|
||||
|
||||
MockInfoPopover.propTypes = {
|
||||
children: PropTypes.node.isRequired,
|
||||
};
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -18,12 +53,18 @@ jest.mock('data/redux/app/selectors', () => ({
|
||||
})),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('data/redux/grading/selectors', () => ({
|
||||
selected: {
|
||||
gradeStatus: jest.fn((...args) => ({ selectedGradeStatus: args })),
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('./RadioCriterion', () => jest.fn((props) => MockRadioCriterion(props)));
|
||||
jest.mock('./ReviewCriterion', () => jest.fn((props) => MockReviewCriterion(props)));
|
||||
jest.mock('./CriterionFeedback', () => jest.fn((props) => MockCriterionFeedback(props)));
|
||||
jest.mock('components/InfoPopover', () => jest.fn((props) => MockInfoPopover(props)));
|
||||
|
||||
describe('Criterion Container', () => {
|
||||
const props = {
|
||||
isGrading: true,
|
||||
@@ -51,53 +92,43 @@ describe('Criterion Container', () => {
|
||||
},
|
||||
gradeStatus: gradeStatuses.ungraded,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<CriterionContainer {...props} />);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('is ungraded and is grading', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
describe('component rendering', () => {
|
||||
it('displays the criterion prompt', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByText('prompt')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is not grading', () => {
|
||||
el = shallow(<CriterionContainer {...props} isGrading={false} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
it('displays all option explanations in the info popover', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
const infoPopover = screen.getByTestId('info-popover');
|
||||
expect(infoPopover).toHaveTextContent('explanation');
|
||||
expect(infoPopover).toHaveTextContent('explanation 2');
|
||||
expect(infoPopover).toHaveTextContent('this label');
|
||||
expect(infoPopover).toHaveTextContent('this label 2');
|
||||
});
|
||||
|
||||
test('is graded and is not grading', () => {
|
||||
el = shallow(<CriterionContainer {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('rendering and all of the option show up', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.instance.findByTestId('help-popover-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
expect(optionEl.props.key).toEqual(props.config.options[i].name);
|
||||
expect(optionEl.children[2].el).toContain(props.config.options[i].explanation);
|
||||
});
|
||||
it('renders RadioCriterion when is ungraded and is grading', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is grading (Radio criterion get render)', () => {
|
||||
const rubricCriteria = el.instance.findByTestId('rubric-criteria')[0];
|
||||
expect(rubricCriteria.children[0].el.type).toEqual('RadioCriterion');
|
||||
it('renders ReviewCriterion when is ungraded and is not grading', () => {
|
||||
render(<CriterionContainer {...props} isGrading={false} />);
|
||||
expect(screen.getByTestId('review-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('radio-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is ungraded and is not grading (Review criterion get render)', () => {
|
||||
el = shallow(<CriterionContainer {...props} isGrading={false} />);
|
||||
const rubricCriteria = el.instance.findByTestId('rubric-criteria')[0];
|
||||
expect(rubricCriteria.children[0].el.type).toEqual('ReviewCriterion');
|
||||
it('renders RadioCriterion when is graded and is not grading', () => {
|
||||
render(<CriterionContainer {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
expect(screen.getByTestId('radio-criterion-component')).toBeInTheDocument();
|
||||
expect(screen.queryByTestId('review-criterion-component')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
test('is graded and is not grading (Radio criterion get render)', () => {
|
||||
el = shallow(<CriterionContainer {...props} isGrading={false} gradeStatus={gradeStatuses.graded} />);
|
||||
const rubricCriteria = el.instance.findByTestId('rubric-criteria')[0];
|
||||
expect(rubricCriteria.children[0].el.type).toEqual('RadioCriterion');
|
||||
it('renders CriterionFeedback component', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByTestId('criterion-feedback-component')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -105,16 +136,18 @@ describe('Criterion Container', () => {
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
|
||||
it('maps rubric criterion config to props', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
test('selectors.grading.selected.gradeStatus', () => {
|
||||
it('maps grading status to props', () => {
|
||||
expect(mapped.gradeStatus).toEqual(
|
||||
selectors.grading.selected.gradeStatus(testState),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { selectors } from 'data/redux';
|
||||
import { DemoWarning, mapStateToProps } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -10,24 +10,26 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
|
||||
describe('DemoWarning component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('does not render if disabled flag is missing', () => {
|
||||
el = shallow(<DemoWarning hide />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
describe('behavior', () => {
|
||||
it('does not render when hide prop is true', () => {
|
||||
const { container } = render(<IntlProvider locale="en"><DemoWarning hide /></IntlProvider>);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
test('snapshot: disabled flag is present', () => {
|
||||
el = shallow(<DemoWarning hide={false} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
|
||||
it('renders alert with warning message when hide prop is false', () => {
|
||||
render(<IntlProvider locale="en"><DemoWarning hide={false} /></IntlProvider>);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveClass('alert-warning');
|
||||
expect(alert).toHaveTextContent(messages.demoModeMessage.defaultMessage);
|
||||
expect(alert).toHaveTextContent(messages.demoModeHeading.defaultMessage);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
test('hide is forwarded from app.isEnabled', () => {
|
||||
it('maps hide prop from app.isEnabled selector', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
expect(mapStateToProps(testState).hide).toEqual(
|
||||
selectors.app.isEnabled(testState),
|
||||
);
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `null`;
|
||||
|
||||
exports[`DemoWarning component snapshots snapshot: disabled flag is present 1`] = `
|
||||
<Alert
|
||||
className="mb-0 rounded-0"
|
||||
variant="warning"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Demo Mode"
|
||||
description="Demo mode heading"
|
||||
id="ora-grading.ReviewModal.demoHeading"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="You are demoing the new ORA staff grading experience. You will be unable to submit grades until you activate the feature. This will become the default grading experience on May 9th (05/09/2022). To opt-in early, or opt-out, please contact Partner Support."
|
||||
description="Demo mode message"
|
||||
id="ora-grading.ReviewModal.demoMessage"
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,33 +1,38 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import EmptySubmission from './EmptySubmission';
|
||||
|
||||
jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `openResponseUrl(${courseId})`,
|
||||
}));
|
||||
|
||||
jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg');
|
||||
|
||||
let el;
|
||||
jest.mock('./assets/empty-state.svg', () => './assets/empty-state.svg');
|
||||
|
||||
describe('EmptySubmission component', () => {
|
||||
describe('component', () => {
|
||||
const props = { courseId: 'test-course-id' };
|
||||
beforeEach(() => {
|
||||
el = shallow(<EmptySubmission {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('openResponse destination', () => {
|
||||
expect(
|
||||
el.instance.findByType(Hyperlink)[0].props.destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
});
|
||||
const props = { courseId: 'test-course-id' };
|
||||
|
||||
it('renders the empty state image with correct alt text', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByAltText('empty state')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders the no results found title message', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByText('Nothing here yet')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders hyperlink with correct destination URL', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
const hyperlink = screen.getByRole('link');
|
||||
expect(hyperlink).toHaveAttribute(
|
||||
'href',
|
||||
urls.openResponse(props.courseId),
|
||||
);
|
||||
});
|
||||
|
||||
it('renders the back to responses button', () => {
|
||||
renderWithIntl(<EmptySubmission {...props} />);
|
||||
expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,54 +1,18 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import PropTypes from 'prop-types';
|
||||
import { render } from '@testing-library/react';
|
||||
import { DataTableContext } from '@openedx/paragon';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
const fieldIds = [
|
||||
'field-id-0',
|
||||
'field-id-1',
|
||||
'field-id-2',
|
||||
'field-id-3',
|
||||
];
|
||||
const fieldIds = ['field-id-0', 'field-id-1', 'field-id-2', 'field-id-3'];
|
||||
const filterOrder = [1, 0, 3, 2];
|
||||
const filters = filterOrder.map(v => ({ id: fieldIds[v] }));
|
||||
const headers = [0, 1, 2, 3].map(v => ({
|
||||
const filters = filterOrder.map((v) => ({ id: fieldIds[v] }));
|
||||
const headers = [0, 1, 2, 3].map((v) => ({
|
||||
id: fieldIds[v],
|
||||
Header: `HeaDer-${v}`,
|
||||
}));
|
||||
|
||||
describe('FilterStatusComponent hooks', () => {
|
||||
const context = { headers, state: { filters } };
|
||||
const mockTableContext = (newContext) => {
|
||||
React.useContext.mockReturnValueOnce(newContext);
|
||||
};
|
||||
beforeEach(() => {
|
||||
context.setAllFilters = jest.fn();
|
||||
});
|
||||
it('returns empty dict if setAllFilters or state.filters is falsey', () => {
|
||||
mockTableContext({ ...context, setAllFilters: null });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
mockTableContext({ ...context, state: { filters: null } });
|
||||
expect(module.filterHooks()).toEqual({});
|
||||
});
|
||||
describe('clearFilters', () => {
|
||||
it('uses React.useCallback to clear filters, only once', () => {
|
||||
mockTableContext(context);
|
||||
const { cb, prereqs } = module.filterHooks().clearFilters.useCallback;
|
||||
expect(prereqs).toEqual([context.setAllFilters]);
|
||||
expect(context.setAllFilters).not.toHaveBeenCalled();
|
||||
cb();
|
||||
expect(context.setAllFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
describe('filterNames', () => {
|
||||
it('returns list of Header values by filter order', () => {
|
||||
mockTableContext(context);
|
||||
expect(module.filterHooks().filterNames).toEqual(
|
||||
filterOrder.map(v => headers[v].Header),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('FilterStatusComponent component', () => {
|
||||
const props = {
|
||||
className: 'css-class-name',
|
||||
@@ -58,34 +22,98 @@ describe('FilterStatusComponent component', () => {
|
||||
buttonClassName: 'css-class-name-for-button',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
const hookProps = {
|
||||
clearFilters: jest.fn().mockName('hookProps.clearFilters'),
|
||||
filterNames: ['filter-name-0', 'filter-name-1'],
|
||||
};
|
||||
const { FilterStatusComponent } = module;
|
||||
const mockHooks = (value) => {
|
||||
jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value);
|
||||
|
||||
const renderWithContext = (contextValue, componentProps = props) => {
|
||||
const TestWrapper = ({ children }) => (
|
||||
<DataTableContext.Provider value={contextValue}>
|
||||
{children}
|
||||
</DataTableContext.Provider>
|
||||
);
|
||||
TestWrapper.propTypes = {
|
||||
children: PropTypes.node,
|
||||
};
|
||||
return render(
|
||||
<TestWrapper>
|
||||
<FilterStatusComponent {...componentProps} />
|
||||
</TestWrapper>,
|
||||
);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
describe('with filters', () => {
|
||||
test('showFilteredFields', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('showFilteredFields=false - hide filterTexts', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(
|
||||
<FilterStatusComponent {...props} showFilteredFields={false} />,
|
||||
);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('does not render when there are no filters', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters: null },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('does not render when setAllFilters is not available', () => {
|
||||
const contextValue = { headers, state: { filters }, setAllFilters: null };
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
|
||||
it('renders clear filters button with correct text when filters exist', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
expect(getByText(props.clearFiltersText)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays filtered field names when showFilteredFields is true', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
const expectedFilterNames = filterOrder.map((v) => headers[v].Header);
|
||||
expectedFilterNames.forEach((name) => {
|
||||
expect(getByText(name, { exact: false })).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
test('without filters', () => {
|
||||
mockHooks({});
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
|
||||
it('does not display filtered field names when showFilteredFields is false', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { queryByText } = renderWithContext(contextValue, {
|
||||
...props,
|
||||
showFilteredFields: false,
|
||||
});
|
||||
expect(queryByText(/Filtered by/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies correct CSS classes to the component', () => {
|
||||
const contextValue = {
|
||||
headers,
|
||||
state: { filters },
|
||||
setAllFilters: jest.fn(),
|
||||
};
|
||||
const { container } = renderWithContext(contextValue);
|
||||
expect(container.firstChild).toHaveClass(props.className);
|
||||
});
|
||||
|
||||
it('calls setAllFilters with empty array when clear button is clicked', () => {
|
||||
const setAllFilters = jest.fn();
|
||||
const contextValue = { headers, state: { filters }, setAllFilters };
|
||||
const { getByText } = renderWithContext(contextValue);
|
||||
const clearButton = getByText(props.clearFiltersText);
|
||||
clearButton.click();
|
||||
expect(setAllFilters).toHaveBeenCalledWith([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,14 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
ListError,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './ListError';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ListError, mapDispatchToProps, mapStateToProps } from './ListError';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
courseId: jest.fn((state) => state.courseId || 'test-course-id'),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
@@ -27,41 +22,60 @@ jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `api/openResponse/${courseId}`,
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
describe('ListError component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.loadSelectionForReview = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
props.initializeApp = jest.fn();
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
initializeApp: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders error alert with proper styling', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const alert = screen.getByRole('alert');
|
||||
expect(alert).toBeInTheDocument();
|
||||
expect(alert).toHaveClass('alert-danger');
|
||||
});
|
||||
describe('render tests', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListError {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('displays error heading and message', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const heading = screen.getByRole('alert').querySelector('.alert-heading');
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveTextContent(messages.loadErrorHeading.defaultMessage);
|
||||
});
|
||||
|
||||
it('displays try again button', () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass('btn-primary');
|
||||
});
|
||||
|
||||
it('calls initializeApp when try again button is clicked', async () => {
|
||||
renderWithIntl(<ListError {...props} />);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button');
|
||||
await user.click(button);
|
||||
expect(props.initializeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
|
||||
it('maps initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(
|
||||
thunkActions.app.initialize,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
span.pgn__icon.breadcrumb-arrow {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
};
|
||||
|
||||
.empty-submission {
|
||||
width: map-get($container-max-widths, "sm");
|
||||
width: var(--pgn-size-container-max-width-sm);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -15,7 +13,7 @@ span.pgn__icon.breadcrumb-arrow {
|
||||
margin: auto;
|
||||
|
||||
> img {
|
||||
padding: map-get($spacers, 5);
|
||||
padding: var(--pgn-spacing-spacer-5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,4 +23,14 @@ span.pgn__icon.breadcrumb-arrow {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (--pgn-size-breakpoint-max-width-xs) {
|
||||
.badge {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.pgn__table-actions > div:first-of-type {
|
||||
z-index: var(--pgn-elevation-modal-zindex) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import * as constants from 'data/constants/app';
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import {
|
||||
ListViewBreadcrumb,
|
||||
mapStateToProps,
|
||||
@@ -15,9 +9,9 @@ import {
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
courseId: jest.fn((state) => state.courseId || 'test-course-id'),
|
||||
ora: {
|
||||
name: (...args) => ({ oraName: args }),
|
||||
name: jest.fn((state) => state.oraName || 'test-ora-name'),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -28,41 +22,60 @@ jest.mock('data/services/lms/urls', () => ({
|
||||
ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`,
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.mock('data/constants/app', () => ({
|
||||
locationId: () => 'test-location-id',
|
||||
}));
|
||||
|
||||
describe('ListViewBreadcrumb component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
oraName: 'fake-ora-name',
|
||||
};
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListViewBreadcrumb {...props} />);
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
oraName: 'fake-ora-name',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders back to responses link with correct destination', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
const backLink = screen.getAllByRole('link').find(
|
||||
link => link.getAttribute('href') === `openResponseUrl(${props.courseId})`,
|
||||
);
|
||||
expect(backLink).toBeInTheDocument();
|
||||
});
|
||||
test('snapshot: empty (no list data)', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
|
||||
it('displays ORA name in heading', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
const heading = screen.getByText(props.oraName);
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveClass('h3');
|
||||
});
|
||||
test('openResponse destination', () => {
|
||||
expect(
|
||||
el.instance.findByType(Hyperlink)[0].props.destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
|
||||
it('renders ORA link with correct destination', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
const oraLink = screen.getAllByRole('link').find(
|
||||
link => link.getAttribute('href') === `oraUrl(${props.courseId}, test-location-id)`,
|
||||
);
|
||||
expect(oraLink).toBeInTheDocument();
|
||||
});
|
||||
test('ora destination', () => {
|
||||
expect(
|
||||
el.instance.findByType(Hyperlink)[1].props.destination,
|
||||
).toEqual(urls.ora(props.courseId, constants.locationId()));
|
||||
|
||||
it('displays back to responses text', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
test('oraName loads from app.ora.name', () => {
|
||||
|
||||
it('maps oraName from app.ora.name selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,20 +1,33 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { SelectedBulkAction } from './SelectedBulkAction';
|
||||
|
||||
describe('SelectedBulkAction component', () => {
|
||||
const props = {
|
||||
selectedFlatRows: [{ id: 1 }, { id: 2 }],
|
||||
handleClick: jest.fn(),
|
||||
handleClick: jest.fn(() => () => {}),
|
||||
};
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<SelectedBulkAction {...props} handleClick={() => jest.fn()} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('handleClick', () => {
|
||||
shallow(<SelectedBulkAction {...props} />);
|
||||
it('renders button with correct text and selected count', () => {
|
||||
renderWithIntl(<SelectedBulkAction {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent(`View selected responses (${props.selectedFlatRows.length})`);
|
||||
});
|
||||
|
||||
it('applies correct CSS class to button', () => {
|
||||
renderWithIntl(<SelectedBulkAction {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('view-selected-responses-btn');
|
||||
expect(button).toHaveClass('btn-primary');
|
||||
});
|
||||
|
||||
it('calls handleClick with selectedFlatRows on render', () => {
|
||||
renderWithIntl(<SelectedBulkAction {...props} />);
|
||||
expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
TextFilter,
|
||||
MultiSelectDropdownFilter,
|
||||
} from '@openedx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { gradingStatuses, submissionFields } from 'data/services/lms/constants';
|
||||
import lmsMessages from 'data/services/lms/messages';
|
||||
@@ -25,113 +25,108 @@ import messages from './messages';
|
||||
/**
|
||||
* <SubmissionsTable />
|
||||
*/
|
||||
export class SubmissionsTable extends React.Component {
|
||||
get gradeStatusOptions() {
|
||||
return Object.keys(gradingStatuses).map(statusKey => ({
|
||||
name: this.translate(lmsMessages[gradingStatuses[statusKey]]),
|
||||
value: gradingStatuses[statusKey],
|
||||
}));
|
||||
}
|
||||
export const SubmissionsTable = ({
|
||||
isIndividual,
|
||||
listData,
|
||||
loadSelectionForReview,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
get userLabel() {
|
||||
return this.translate(this.props.isIndividual ? messages.username : messages.teamName);
|
||||
}
|
||||
const translate = (...args) => intl.formatMessage(...args);
|
||||
|
||||
get userAccessor() {
|
||||
return this.props.isIndividual
|
||||
? submissionFields.username
|
||||
: submissionFields.teamName;
|
||||
}
|
||||
const gradeStatusOptions = Object.keys(gradingStatuses).map(statusKey => ({
|
||||
name: translate(lmsMessages[gradingStatuses[statusKey]]),
|
||||
value: gradingStatuses[statusKey],
|
||||
}));
|
||||
|
||||
get dateSubmittedLabel() {
|
||||
return this.translate(this.props.isIndividual
|
||||
? messages.learnerSubmissionDate
|
||||
: messages.teamSubmissionDate);
|
||||
}
|
||||
const userLabel = translate(isIndividual ? messages.username : messages.teamName);
|
||||
|
||||
formatDate = ({ value }) => {
|
||||
const userAccessor = isIndividual
|
||||
? submissionFields.username
|
||||
: submissionFields.teamName;
|
||||
|
||||
const dateSubmittedLabel = translate(isIndividual
|
||||
? messages.learnerSubmissionDate
|
||||
: messages.teamSubmissionDate);
|
||||
|
||||
const formatDate = ({ value }) => {
|
||||
const date = new Date(moment(value));
|
||||
return date.toLocaleString();
|
||||
};
|
||||
|
||||
formatGrade = ({ value: score }) => (
|
||||
const formatGrade = ({ value: score }) => (
|
||||
score === null ? '-' : `${score.pointsEarned}/${score.pointsPossible}`
|
||||
);
|
||||
|
||||
formatStatus = ({ value }) => (<StatusBadge status={value} />);
|
||||
const formatStatus = ({ value }) => (<StatusBadge status={value} />);
|
||||
|
||||
translate = (...args) => this.props.intl.formatMessage(...args);
|
||||
|
||||
handleViewAllResponsesClick = (data) => () => {
|
||||
const handleViewAllResponsesClick = (data) => () => {
|
||||
const getSubmissionUUID = (row) => row.original.submissionUUID;
|
||||
this.props.loadSelectionForReview(data.map(getSubmissionUUID));
|
||||
loadSelectionForReview(data.map(getSubmissionUUID));
|
||||
};
|
||||
|
||||
render() {
|
||||
if (!this.props.listData.length) {
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div className="submissions-table">
|
||||
<DataTable
|
||||
data-testid="data-table"
|
||||
isFilterable
|
||||
FilterStatusComponent={FilterStatusComponent}
|
||||
numBreakoutFilters={2}
|
||||
defaultColumnValues={{ Filter: TextFilter }}
|
||||
isSelectable
|
||||
isSortable
|
||||
isPaginated
|
||||
itemCount={this.props.listData.length}
|
||||
initialState={{ pageSize: 10, pageIndex: 0 }}
|
||||
data={this.props.listData}
|
||||
tableActions={[
|
||||
<TableAction handleClick={this.handleViewAllResponsesClick} />,
|
||||
]}
|
||||
bulkActions={[
|
||||
<SelectedBulkAction handleClick={this.handleViewAllResponsesClick} />,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
Header: this.userLabel,
|
||||
accessor: this.userAccessor,
|
||||
},
|
||||
{
|
||||
Header: this.dateSubmittedLabel,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: this.formatDate,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.grade),
|
||||
accessor: submissionFields.score,
|
||||
Cell: this.formatGrade,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: this.translate(messages.gradingStatus),
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: this.formatStatus,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: this.gradeStatusOptions,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
if (!listData.length) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="submissions-table">
|
||||
<DataTable
|
||||
data-testid="data-table"
|
||||
isFilterable
|
||||
FilterStatusComponent={FilterStatusComponent}
|
||||
numBreakoutFilters={2}
|
||||
defaultColumnValues={{ Filter: TextFilter }}
|
||||
isSelectable
|
||||
isSortable
|
||||
isPaginated
|
||||
itemCount={listData.length}
|
||||
initialState={{ pageSize: 10, pageIndex: 0 }}
|
||||
data={listData}
|
||||
tableActions={[
|
||||
<TableAction handleClick={handleViewAllResponsesClick} />,
|
||||
]}
|
||||
bulkActions={[
|
||||
<SelectedBulkAction handleClick={handleViewAllResponsesClick} />,
|
||||
]}
|
||||
columns={[
|
||||
{
|
||||
Header: userLabel,
|
||||
accessor: userAccessor,
|
||||
},
|
||||
{
|
||||
Header: dateSubmittedLabel,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: formatDate,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: translate(messages.grade),
|
||||
accessor: submissionFields.score,
|
||||
Cell: formatGrade,
|
||||
disableFilters: true,
|
||||
},
|
||||
{
|
||||
Header: translate(messages.gradingStatus),
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: formatStatus,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: gradeStatusOptions,
|
||||
},
|
||||
]}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
SubmissionsTable.defaultProps = {
|
||||
listData: [],
|
||||
};
|
||||
SubmissionsTable.propTypes = {
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
isIndividual: PropTypes.bool.isRequired,
|
||||
listData: PropTypes.arrayOf(PropTypes.shape({
|
||||
@@ -155,4 +150,4 @@ export const mapDispatchToProps = {
|
||||
loadSelectionForReview: thunkActions.grading.loadSelectionForReview,
|
||||
};
|
||||
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(SubmissionsTable));
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(SubmissionsTable);
|
||||
|
||||
@@ -1,48 +1,31 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import {
|
||||
MultiSelectDropdownFilter,
|
||||
TextFilter,
|
||||
} from '@openedx/paragon';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { gradingStatuses as statuses, submissionFields } from 'data/services/lms/constants';
|
||||
|
||||
import StatusBadge from 'components/StatusBadge';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import messages from './messages';
|
||||
import { gradingStatuses as statuses } from 'data/services/lms/constants';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import {
|
||||
SubmissionsTable,
|
||||
mapStateToProps,
|
||||
mapDispatchToProps,
|
||||
} from './SubmissionsTable';
|
||||
|
||||
jest.mock('./FilterStatusComponent', () => jest.fn().mockName('FilterStatusComponent'));
|
||||
jest.mock('./TableAction', () => jest.fn().mockName('TableAction'));
|
||||
jest.mock('./SelectedBulkAction', () => jest.fn().mockName('SelectedBulkAction'));
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
ora: {
|
||||
isIndividual: (...args) => ({ isIndividual: args }),
|
||||
isIndividual: jest.fn((state) => state.isIndividual || true),
|
||||
},
|
||||
},
|
||||
submissions: {
|
||||
listData: (...args) => ({ listData: args }),
|
||||
listData: jest.fn((state) => state.listData || []),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
grading: {
|
||||
loadSelectionForReview: (...args) => ({ loadSelectionForReview: args }),
|
||||
loadSelectionForReview: jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
const dates = [
|
||||
'2021-12-08 09:06:15.319213+00:00',
|
||||
'2021-12-10 18:06:15.319213+00:00',
|
||||
@@ -64,18 +47,15 @@ const individualData = [
|
||||
dateSubmitted: dates[1],
|
||||
gradingStatus: statuses.graded,
|
||||
score: {
|
||||
pointsEarned: 2,
|
||||
pointsEarned: 9,
|
||||
pointsPossible: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
username: 'username-3',
|
||||
dateSubmitted: dates[2],
|
||||
gradingStatus: statuses.inProgress,
|
||||
score: {
|
||||
pointsEarned: 3,
|
||||
pointsPossible: 10,
|
||||
},
|
||||
username: 'username-2',
|
||||
dateSubmitted: dates[1],
|
||||
gradingStatus: statuses.ungraded,
|
||||
score: null,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -98,206 +78,88 @@ const teamData = [
|
||||
pointsPossible: 10,
|
||||
},
|
||||
},
|
||||
{
|
||||
teamName: 'teamName-3',
|
||||
dateSubmitted: dates[2],
|
||||
gradingStatus: statuses.inProgress,
|
||||
score: {
|
||||
pointsEarned: 3,
|
||||
pointsPossible: 10,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
describe('SubmissionsTable component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
isIndividual: true,
|
||||
listData: [...individualData],
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.loadSelectionForReview = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
const defaultProps = {
|
||||
isIndividual: true,
|
||||
listData: [...individualData],
|
||||
loadSelectionForReview: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders DataTable component', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
const submissionsTable = screen.getByRole('table');
|
||||
expect(submissionsTable).toBeInTheDocument();
|
||||
});
|
||||
describe('render tests', () => {
|
||||
const mockMethod = (methodName) => {
|
||||
el.instance[methodName] = jest.fn().mockName(`this.${methodName}`);
|
||||
};
|
||||
beforeEach(() => {
|
||||
el = shallow(<SubmissionsTable {...props} />);
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
mockMethod('handleViewAllResponsesClick');
|
||||
mockMethod('formatDate');
|
||||
mockMethod('formatGrade');
|
||||
mockMethod('formatStatus');
|
||||
});
|
||||
test('snapshot: empty (no list data)', () => {
|
||||
el = shallow(<SubmissionsTable {...props} listData={[]} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: happy path', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('snapshot: team happy path', () => {
|
||||
el = shallow(<SubmissionsTable {...props} isIndividual={false} listData={[...teamData]} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe('DataTable', () => {
|
||||
let tableProps;
|
||||
beforeEach(() => {
|
||||
tableProps = el.instance.findByTestId('data-table')[0].props;
|
||||
});
|
||||
test.each([
|
||||
'isFilterable',
|
||||
'isSelectable',
|
||||
'isSortable',
|
||||
'isPaginated',
|
||||
])('%s', key => expect(tableProps[key]).toEqual(true));
|
||||
test.each([
|
||||
['numBreakoutFilters', 2],
|
||||
['defaultColumnValues', { Filter: TextFilter }],
|
||||
['itemCount', 3],
|
||||
['initialState', { pageSize: 10, pageIndex: 0 }],
|
||||
])('%s = %p', (key, value) => expect(tableProps[key]).toEqual(value));
|
||||
describe('individual columns', () => {
|
||||
let columns;
|
||||
beforeEach(() => {
|
||||
columns = tableProps.columns;
|
||||
});
|
||||
test('username column', () => {
|
||||
expect(columns[0]).toEqual({
|
||||
Header: messages.username.defaultMessage,
|
||||
accessor: submissionFields.username,
|
||||
});
|
||||
});
|
||||
test('submission date column', () => {
|
||||
expect(columns[1]).toEqual({
|
||||
Header: messages.learnerSubmissionDate.defaultMessage,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: el.instance.children[0].props.columns[1].Cell,
|
||||
disableFilters: true,
|
||||
});
|
||||
});
|
||||
test('grade column', () => {
|
||||
expect(columns[2]).toEqual({
|
||||
Header: messages.grade.defaultMessage,
|
||||
accessor: submissionFields.score,
|
||||
Cell: el.instance.children[0].props.columns[2].Cell,
|
||||
disableFilters: true,
|
||||
});
|
||||
});
|
||||
test('grading status column', () => {
|
||||
expect(columns[3]).toEqual({
|
||||
Header: messages.gradingStatus.defaultMessage,
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: el.instance.children[0].props.columns[3].Cell,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: el.instance.children[0].props.columns[3].filterChoices,
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('team columns', () => {
|
||||
let columns;
|
||||
beforeEach(() => {
|
||||
el = shallow(<SubmissionsTable {...props} isIndividual={false} listData={[...teamData]} />);
|
||||
columns = el.instance.findByTestId('data-table')[0].props.columns;
|
||||
});
|
||||
test('teamName column', () => {
|
||||
expect(columns[0]).toEqual({
|
||||
Header: messages.teamName.defaultMessage,
|
||||
accessor: submissionFields.teamName,
|
||||
});
|
||||
});
|
||||
test('submission date column', () => {
|
||||
expect(columns[1]).toEqual({
|
||||
Header: messages.teamSubmissionDate.defaultMessage,
|
||||
accessor: submissionFields.dateSubmitted,
|
||||
Cell: el.instance.children[0].props.columns[1].Cell,
|
||||
disableFilters: true,
|
||||
});
|
||||
});
|
||||
test('grade column', () => {
|
||||
expect(columns[2]).toEqual({
|
||||
Header: messages.grade.defaultMessage,
|
||||
accessor: submissionFields.score,
|
||||
Cell: el.instance.children[0].props.columns[2].Cell,
|
||||
disableFilters: true,
|
||||
});
|
||||
});
|
||||
test('grading status column', () => {
|
||||
expect(columns[3]).toEqual({
|
||||
Header: messages.gradingStatus.defaultMessage,
|
||||
accessor: submissionFields.gradingStatus,
|
||||
Cell: el.instance.children[0].props.columns[3].Cell,
|
||||
Filter: MultiSelectDropdownFilter,
|
||||
filter: 'includesValue',
|
||||
filterChoices: el.instance.children[0].props.columns[3].filterChoices,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns empty render when no list data provided', () => {
|
||||
const { container } = renderWithIntl(<SubmissionsTable {...defaultProps} listData={[]} />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
describe('formatDate method', () => {
|
||||
it('returns the date in locale time string', () => {
|
||||
const fakeDate = 16131215154955;
|
||||
const fakeDateString = 'test-date-string';
|
||||
const mock = jest.spyOn(Date.prototype, 'toLocaleString').mockReturnValue(fakeDateString);
|
||||
expect(el.instance.children[0].props.columns[1].Cell({ value: fakeDate })).toEqual(fakeDateString);
|
||||
mock.mockRestore();
|
||||
});
|
||||
});
|
||||
describe('formatGrade method', () => {
|
||||
it('returns "-" if grade is null', () => {
|
||||
expect(el.instance.children[0].props.columns[2].Cell({ value: null })).toEqual('-');
|
||||
});
|
||||
it('returns <pointsEarned>/<pointsPossible> if grade exists', () => {
|
||||
expect(
|
||||
el.instance.children[0].props.columns[2].Cell({ value: { pointsEarned: 1, pointsPossible: 10 } }),
|
||||
).toEqual('1/10');
|
||||
});
|
||||
});
|
||||
describe('formatStatus method', () => {
|
||||
it('returns a StatusBadge with the given status', () => {
|
||||
const status = 'graded';
|
||||
expect(el.instance.children[0].props.columns[3].Cell({ value: 'graded' })).toEqual(
|
||||
<StatusBadge status={status} />,
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('handleViewAllResponsesClick', () => {
|
||||
it('calls loadSelectionForReview with submissionUUID from all rows if there are no selectedRows', () => {
|
||||
const data = [
|
||||
{ original: { submissionUUID: '123' } },
|
||||
{ original: { submissionUUID: '456' } },
|
||||
{ original: { submissionUUID: '789' } },
|
||||
];
|
||||
el.instance.children[0].props.tableActions[0].props.handleClick(data)();
|
||||
expect(el.shallowRenderer._instance.props.loadSelectionForReview).toHaveBeenCalledWith(['123', '456', '789']); // eslint-disable-line no-underscore-dangle
|
||||
});
|
||||
});
|
||||
|
||||
it('renders individual columns for individual submissions', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
expect(screen.getByText('Username')).toBeInTheDocument();
|
||||
expect(screen.getByText('Learner submission date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders team columns for team submissions', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} isIndividual={false} listData={teamData} />);
|
||||
expect(screen.getByText('Team name')).toBeInTheDocument();
|
||||
expect(screen.getByText('Team submission date')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats date correctly', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
const formattedDate = screen.getAllByText('12/10/2021, 6:06:15 PM');
|
||||
expect(formattedDate.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it('formats grade as dash when null', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
const ungradedBadge = screen.getAllByText('Ungraded')[1].parentElement;
|
||||
const score = ungradedBadge.previousSibling;
|
||||
expect(score).toHaveTextContent('-');
|
||||
});
|
||||
|
||||
it('formats grade as points earned over points possible', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
const scored = screen.getByText('9/10');
|
||||
expect(scored).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('formats status as StatusBadge component', () => {
|
||||
renderWithIntl(<SubmissionsTable {...defaultProps} />);
|
||||
const gradedBadge = screen.getByText('Grading Completed');
|
||||
expect(gradedBadge).toHaveClass('badge-success');
|
||||
const ungradedBadge = screen.getAllByText('Ungraded')[0];
|
||||
expect(ungradedBadge).toHaveClass('badge-primary');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('listData loads from submissions.listData', () => {
|
||||
|
||||
it('maps listData from submissions.listData selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.listData).toEqual(selectors.submissions.listData(testState));
|
||||
});
|
||||
|
||||
it('maps isIndividual from app.ora.isIndividual selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
expect(mapped.isIndividual).toEqual(selectors.app.ora.isIndividual(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads loadSelectionForReview from thunkActions.grading.loadSelectionForReview', () => {
|
||||
expect(
|
||||
mapDispatchToProps.loadSelectionForReview,
|
||||
).toEqual(thunkActions.grading.loadSelectionForReview);
|
||||
it('maps loadSelectionForReview from thunkActions', () => {
|
||||
expect(mapDispatchToProps.loadSelectionForReview).toEqual(thunkActions.grading.loadSelectionForReview);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,29 +1,50 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { TableAction } from './TableAction';
|
||||
import messages from './messages';
|
||||
|
||||
describe('TableAction component', () => {
|
||||
const props = {
|
||||
tableInstance: { rows: [{ id: 1 }, { id: 2 }] },
|
||||
handleClick: jest.fn(),
|
||||
handleClick: jest.fn(() => () => {}),
|
||||
};
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<TableAction {...props} handleClick={() => jest.fn()} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('Inactive Button "View All Responses"', () => {
|
||||
it('renders button with correct text', () => {
|
||||
renderWithIntl(<TableAction {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent(messages.viewAllResponses.defaultMessage);
|
||||
});
|
||||
|
||||
it('applies correct CSS class to button', () => {
|
||||
renderWithIntl(<TableAction {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('view-all-responses-btn');
|
||||
expect(button).toHaveClass('btn-primary');
|
||||
});
|
||||
|
||||
it('enables button when rows are present', () => {
|
||||
renderWithIntl(<TableAction {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).not.toBeDisabled();
|
||||
});
|
||||
|
||||
it('disables button when no rows are present', () => {
|
||||
const emptyProps = {
|
||||
tableInstance: { rows: [] },
|
||||
handleClick: jest.fn(),
|
||||
handleClick: jest.fn(() => () => {}),
|
||||
};
|
||||
const el = shallow(<TableAction {...emptyProps} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
renderWithIntl(<TableAction {...emptyProps} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeDisabled();
|
||||
});
|
||||
|
||||
test('handleClick', () => {
|
||||
shallow(<TableAction {...props} />);
|
||||
it('calls handleClick with table rows on render', () => {
|
||||
renderWithIntl(<TableAction {...props} />);
|
||||
expect(props.handleClick).toHaveBeenCalledWith(props.tableInstance.rows);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`EmptySubmission component component snapshot 1`] = `
|
||||
<div
|
||||
className="empty-submission"
|
||||
>
|
||||
<img
|
||||
alt="empty state"
|
||||
src="./assets/emptyState.svg"
|
||||
/>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
defaultMessage="Nothing here yet"
|
||||
description="Empty table for the submission table title"
|
||||
id="ora-grading.ListView.noResultsFoundTitle"
|
||||
/>
|
||||
</h3>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="When learners submit responses, they will appear here"
|
||||
description="Empty table messages"
|
||||
id="ora-grading.ListView.noResultsFoundBody"
|
||||
/>
|
||||
</p>
|
||||
<Hyperlink
|
||||
className="py-4"
|
||||
destination="openResponseUrl(test-course-id)"
|
||||
>
|
||||
<Button
|
||||
variant="outline-primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to all open responses"
|
||||
description="Breadcrumbs link text to return to ORA list in LMS"
|
||||
id="ora-grading.ListView.ListViewBreadcrumbs.backToResponses"
|
||||
/>
|
||||
</Button>
|
||||
</Hyperlink>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,37 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilterStatusComponent component snapshot with filters showFilteredFields 1`] = `
|
||||
<div
|
||||
className="css-class-name"
|
||||
>
|
||||
<p>
|
||||
Filtered by
|
||||
filter-name-0, filter-name-1
|
||||
</p>
|
||||
<Button
|
||||
className="css-class-name-for-button"
|
||||
onClick={[MockFunction hookProps.clearFilters]}
|
||||
size="button-size"
|
||||
variant="button-variant"
|
||||
>
|
||||
clear-filter-text
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterStatusComponent component snapshot with filters showFilteredFields=false - hide filterTexts 1`] = `
|
||||
<div
|
||||
className="css-class-name"
|
||||
>
|
||||
<Button
|
||||
className="css-class-name-for-button"
|
||||
onClick={[MockFunction hookProps.clearFilters]}
|
||||
size="button-size"
|
||||
variant="button-variant"
|
||||
>
|
||||
clear-filter-text
|
||||
</Button>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`FilterStatusComponent component snapshot without filters 1`] = `null`;
|
||||
@@ -1,48 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListError component component render tests snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
[
|
||||
<Button
|
||||
onClick={[MockFunction]}
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Reload submissions"
|
||||
description="Reload button text in case of network failure"
|
||||
id="ora-grading.ListView.reloadSubmissions"
|
||||
/>
|
||||
</Button>,
|
||||
]
|
||||
}
|
||||
variant="danger"
|
||||
>
|
||||
<Alert.Heading>
|
||||
<FormattedMessage
|
||||
defaultMessage="Error loading submissions"
|
||||
description="Initialization failure alert header"
|
||||
id="ora-grading.ListView.loadErrorHeading"
|
||||
/>
|
||||
</Alert.Heading>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
defaultMessage="An error occurred while loading the submissions for this response. Try reloading the page or going {backToResponses}."
|
||||
description="Initialization failure alert message line 2"
|
||||
id="ora-grading.ListView.loadErrorMessage1"
|
||||
values={
|
||||
{
|
||||
"backToResponses": <Hyperlink
|
||||
destination="api/openResponse/test-course-id"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="back to all Open Responses"
|
||||
description="lowercase string for link to list of all open responses in lms"
|
||||
id="ora-grading.ListView.backToResponsesLowercase"
|
||||
/>
|
||||
</Hyperlink>,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</p>
|
||||
</Alert>
|
||||
`;
|
||||
@@ -1,38 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListViewBreadcrumb component component snapshot: empty (no list data) 1`] = `
|
||||
<Fragment>
|
||||
<Hyperlink
|
||||
className="py-4"
|
||||
destination="openResponseUrl(test-course-id)"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block mr-3 breadcrumb-arrow"
|
||||
src={[MockFunction icons.ArrowBack]}
|
||||
/>
|
||||
<FormattedMessage
|
||||
defaultMessage="Back to all open responses"
|
||||
description="Breadcrumbs link text to return to ORA list in LMS"
|
||||
id="ora-grading.ListView.ListViewBreadcrumbs.backToResponses"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<p
|
||||
className="py-4"
|
||||
>
|
||||
<span
|
||||
className="h3"
|
||||
>
|
||||
fake-ora-name
|
||||
</span>
|
||||
<Hyperlink
|
||||
className="align-middle"
|
||||
destination="oraUrl(test-course-id, fake-location-id)"
|
||||
>
|
||||
<Icon
|
||||
className="d-inline-block"
|
||||
src={[MockFunction icons.Launch]}
|
||||
/>
|
||||
</Hyperlink>
|
||||
</p>
|
||||
</Fragment>
|
||||
`;
|
||||
@@ -1,20 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SelectedBulkAction component snapshots 1`] = `
|
||||
<Button
|
||||
className="view-selected-responses-btn"
|
||||
onClick={[MockFunction]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View selected responses ({value})"
|
||||
description="Button text to load selected responses for review/grading"
|
||||
id="ora-grading.ListView.viewSelectedResponses"
|
||||
values={
|
||||
{
|
||||
"value": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,247 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: empty (no list data) 1`] = `null`;
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: happy path 1`] = `
|
||||
<div
|
||||
className="submissions-table"
|
||||
>
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
[
|
||||
<mockConstructor
|
||||
handleClick={[Function]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
"Header": "Username",
|
||||
"accessor": "username",
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Header": "Learner submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": [
|
||||
{
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
{
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
{
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
{
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
[
|
||||
{
|
||||
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-1",
|
||||
},
|
||||
{
|
||||
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
|
||||
"gradingStatus": "graded",
|
||||
"score": {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-2",
|
||||
},
|
||||
{
|
||||
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"username": "username-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
data-testid="data-table"
|
||||
defaultColumnValues={
|
||||
{
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
{
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
[
|
||||
<mockConstructor
|
||||
handleClick={[Function]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`SubmissionsTable component component render tests snapshots snapshot: team happy path 1`] = `
|
||||
<div
|
||||
className="submissions-table"
|
||||
>
|
||||
<DataTable
|
||||
FilterStatusComponent={[MockFunction FilterStatusComponent]}
|
||||
bulkActions={
|
||||
[
|
||||
<mockConstructor
|
||||
handleClick={[Function]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
"Header": "Team name",
|
||||
"accessor": "teamName",
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Header": "Team submission date",
|
||||
"accessor": "dateSubmitted",
|
||||
"disableFilters": true,
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Header": "Grade",
|
||||
"accessor": "score",
|
||||
"disableFilters": true,
|
||||
},
|
||||
{
|
||||
"Cell": [Function],
|
||||
"Filter": "MultiSelectDropdownFilter",
|
||||
"Header": "Grading status",
|
||||
"accessor": "gradingStatus",
|
||||
"filter": "includesValue",
|
||||
"filterChoices": [
|
||||
{
|
||||
"name": "Ungraded",
|
||||
"value": "ungraded",
|
||||
},
|
||||
{
|
||||
"name": "Grading Completed",
|
||||
"value": "graded",
|
||||
},
|
||||
{
|
||||
"name": "Currently being graded by someone else",
|
||||
"value": "locked",
|
||||
},
|
||||
{
|
||||
"name": "You are currently grading this response",
|
||||
"value": "in-progress",
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
[
|
||||
{
|
||||
"dateSubmitted": "2021-12-08 09:06:15.319213+00:00",
|
||||
"gradingStatus": "ungraded",
|
||||
"score": {
|
||||
"pointsEarned": 1,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-1",
|
||||
},
|
||||
{
|
||||
"dateSubmitted": "2021-12-10 18:06:15.319213+00:00",
|
||||
"gradingStatus": "graded",
|
||||
"score": {
|
||||
"pointsEarned": 2,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-2",
|
||||
},
|
||||
{
|
||||
"dateSubmitted": "2021-12-11 07:06:15.319213+00:00",
|
||||
"gradingStatus": "in-progress",
|
||||
"score": {
|
||||
"pointsEarned": 3,
|
||||
"pointsPossible": 10,
|
||||
},
|
||||
"teamName": "teamName-3",
|
||||
},
|
||||
]
|
||||
}
|
||||
data-testid="data-table"
|
||||
defaultColumnValues={
|
||||
{
|
||||
"Filter": "TextFilter",
|
||||
}
|
||||
}
|
||||
initialState={
|
||||
{
|
||||
"pageIndex": 0,
|
||||
"pageSize": 10,
|
||||
}
|
||||
}
|
||||
isFilterable={true}
|
||||
isPaginated={true}
|
||||
isSelectable={true}
|
||||
isSortable={true}
|
||||
itemCount={3}
|
||||
numBreakoutFilters={2}
|
||||
tableActions={
|
||||
[
|
||||
<mockConstructor
|
||||
handleClick={[Function]}
|
||||
/>,
|
||||
]
|
||||
}
|
||||
>
|
||||
<DataTable.TableControlBar />
|
||||
<DataTable.Table />
|
||||
<DataTable.TableFooter />
|
||||
</DataTable>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,30 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TableAction component Inactive Button "View All Responses" 1`] = `
|
||||
<Button
|
||||
className="view-all-responses-btn"
|
||||
disabled={true}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View all responses"
|
||||
description="Button text to load all responses for review/grading"
|
||||
id="ora-grading.ListView.viewAllResponses"
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
|
||||
exports[`TableAction component snapshots 1`] = `
|
||||
<Button
|
||||
className="view-all-responses-btn"
|
||||
disabled={false}
|
||||
onClick={[MockFunction]}
|
||||
variant="primary"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="View all responses"
|
||||
description="Button text to load all responses for review/grading"
|
||||
id="ora-grading.ListView.viewAllResponses"
|
||||
/>
|
||||
</Button>
|
||||
`;
|
||||
@@ -1,56 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ListView component component snapshots error 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<ListError />
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component snapshots loaded has data 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<Fragment>
|
||||
<ListViewBreadcrumb />
|
||||
<SubmissionsTable />
|
||||
</Fragment>
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component snapshots loaded with no data 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<EmptySubmission
|
||||
courseId="test-course-id"
|
||||
/>
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
|
||||
exports[`ListView component component snapshots loading 1`] = `
|
||||
<Container
|
||||
className="py-4"
|
||||
>
|
||||
<div
|
||||
className="w-100 h-100 text-center"
|
||||
>
|
||||
<Spinner
|
||||
animation="border"
|
||||
variant="primary"
|
||||
/>
|
||||
<h4>
|
||||
<FormattedMessage
|
||||
defaultMessage="Loading responses"
|
||||
description="loading text for submission response list"
|
||||
id="ora-grading.ListView.loadingResponses"
|
||||
/>
|
||||
</h4>
|
||||
</div>
|
||||
<ReviewModal />
|
||||
</Container>
|
||||
`;
|
||||
@@ -1,47 +1,93 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { RequestKeys } from 'data/constants/requests';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ListView, mapStateToProps, mapDispatchToProps } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('components/StatusBadge', () => 'StatusBadge');
|
||||
jest.mock('containers/ReviewModal', () => 'ReviewModal');
|
||||
jest.mock('./ListViewBreadcrumb', () => 'ListViewBreadcrumb');
|
||||
jest.mock('./ListError', () => 'ListError');
|
||||
jest.mock('./SubmissionsTable', () => 'SubmissionsTable');
|
||||
jest.mock('./EmptySubmission', () => 'EmptySubmission');
|
||||
jest.mock('containers/ReviewModal', () => {
|
||||
const ReviewModal = () => <div data-testid="review-modal">ReviewModal</div>;
|
||||
return ReviewModal;
|
||||
});
|
||||
|
||||
jest.mock('./ListViewBreadcrumb', () => {
|
||||
const ListViewBreadcrumb = () => (
|
||||
<div data-testid="breadcrumb">Back to all open responses</div>
|
||||
);
|
||||
return ListViewBreadcrumb;
|
||||
});
|
||||
|
||||
jest.mock('./ListError', () => {
|
||||
const ListError = () => (
|
||||
<div data-testid="list-error">
|
||||
<button type="button">Reload submissions</button>
|
||||
</div>
|
||||
);
|
||||
return ListError;
|
||||
});
|
||||
|
||||
jest.mock('./SubmissionsTable', () => {
|
||||
const SubmissionsTable = () => (
|
||||
<div data-testid="submissions-table">SubmissionsTable</div>
|
||||
);
|
||||
return SubmissionsTable;
|
||||
});
|
||||
|
||||
jest.mock('./EmptySubmission', () => {
|
||||
const EmptySubmission = () => (
|
||||
<div data-testid="empty-submission">
|
||||
<h3>Nothing here yet</h3>
|
||||
<p>When learners submit responses, they will appear here</p>
|
||||
</div>
|
||||
);
|
||||
return EmptySubmission;
|
||||
});
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
isEnabled: () => false,
|
||||
oraName: () => 'Test ORA Name',
|
||||
},
|
||||
requests: {
|
||||
isCompleted: (...args) => ({ isCompleted: args }),
|
||||
isFailed: (...args) => ({ isFailed: args }),
|
||||
allowNavigation: () => true,
|
||||
},
|
||||
submissions: {
|
||||
isEmptySubmissionData: (...args) => ({ isEmptySubmissionData: args }),
|
||||
},
|
||||
grading: {
|
||||
activeIndex: () => 0,
|
||||
selectionLength: () => 1,
|
||||
selected: {
|
||||
submissionUUID: () => null,
|
||||
overallFeedback: () => '',
|
||||
criteria: () => [],
|
||||
},
|
||||
next: {
|
||||
doesExist: () => false,
|
||||
},
|
||||
prev: {
|
||||
doesExist: () => false,
|
||||
},
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
app: {
|
||||
initialize: (...args) => ({ initialize: args }),
|
||||
},
|
||||
grading: {
|
||||
submitGrade: () => jest.fn(),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
jest.mock('@openedx/paragon', () => ({
|
||||
Container: 'Container',
|
||||
Spinner: 'Spinner',
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
describe('ListView component', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
@@ -49,37 +95,75 @@ describe('ListView component', () => {
|
||||
isLoaded: false,
|
||||
hasError: false,
|
||||
isEmptySubmissionData: false,
|
||||
initializeApp: jest.fn(),
|
||||
intl: { formatMessage },
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.initializeApp = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
});
|
||||
describe('snapshots', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListView {...props} />);
|
||||
});
|
||||
test('loading', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('loaded has data', () => {
|
||||
el = shallow(<ListView {...props} isLoaded />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('loaded with no data', () => {
|
||||
el = shallow(<ListView {...props} isLoaded isEmptySubmissionData />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('error', () => {
|
||||
el = shallow(<ListView {...props} hasError />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('calls initializeApp on load', () => {
|
||||
el = shallow(<ListView {...props} />);
|
||||
expect(props.initializeApp).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('displays loading spinner and message when not loaded and no error', () => {
|
||||
renderWithIntl(<ListView {...props} />);
|
||||
|
||||
// Check for loading message
|
||||
expect(screen.getByText(messages.loadingResponses.defaultMessage)).toBeInTheDocument();
|
||||
|
||||
// Check for spinner by finding element with spinner class
|
||||
const spinner = document.querySelector('.pgn__spinner');
|
||||
expect(spinner).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays ListViewBreadcrumb and SubmissionsTable when loaded with data', () => {
|
||||
renderWithIntl(<ListView {...props} isLoaded />);
|
||||
|
||||
expect(
|
||||
screen.getByText('Back to all open responses'),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByTestId('submissions-table')).toBeInTheDocument();
|
||||
expect(screen.queryByText('FormattedMessage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays EmptySubmission component when loaded but has no submission data', () => {
|
||||
renderWithIntl(<ListView {...props} isLoaded isEmptySubmissionData />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('heading', { name: 'Nothing here yet' }),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText(
|
||||
'When learners submit responses, they will appear here',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(
|
||||
screen.queryByText('Back to all open responses'),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.queryByTestId('submissions-table')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays ListError component when there is an error', () => {
|
||||
renderWithIntl(<ListView {...props} hasError />);
|
||||
|
||||
expect(
|
||||
screen.getByRole('button', { name: 'Reload submissions' }),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('FormattedMessage')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('always displays ReviewModal component regardless of state', () => {
|
||||
const { rerender } = renderWithIntl(<ListView {...props} />);
|
||||
expect(screen.getByText('ReviewModal')).toBeInTheDocument();
|
||||
|
||||
rerender(<ListView {...props} isLoaded />);
|
||||
expect(screen.getByText('ReviewModal')).toBeInTheDocument();
|
||||
|
||||
rerender(<ListView {...props} hasError />);
|
||||
expect(screen.getByText('ReviewModal')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('calls initializeApp on component mount', () => {
|
||||
renderWithIntl(<ListView {...props} />);
|
||||
expect(props.initializeApp).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
describe('mapStateToProps', () => {
|
||||
@@ -89,27 +173,27 @@ describe('ListView component', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
test('isLoaded loads from requests.isCompleted', () => {
|
||||
it('maps isLoaded from requests.isCompleted selector', () => {
|
||||
expect(mapped.isLoaded).toEqual(
|
||||
selectors.requests.isCompleted(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
test('hasError loads from requests.isFailed', () => {
|
||||
it('maps hasError from requests.isFailed selector', () => {
|
||||
expect(mapped.hasError).toEqual(
|
||||
selectors.requests.isFailed(testState, { requestKey }),
|
||||
);
|
||||
});
|
||||
test('isEmptySubmissionData loads from submissions.isEmptySubmissionData', () => {
|
||||
it('maps isEmptySubmissionData from submissions.isEmptySubmissionData selector', () => {
|
||||
expect(mapped.isEmptySubmissionData).toEqual(
|
||||
selectors.submissions.isEmptySubmissionData(testState),
|
||||
);
|
||||
});
|
||||
});
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads initializeApp from thunkActions.app.initialize', () => {
|
||||
it('maps initializeApp to thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(
|
||||
thunkActions.app.initialize,
|
||||
);
|
||||
|
||||
@@ -1,31 +0,0 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { NotificationsBanner } from '.';
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('NotificationsBanner component', () => {
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
test('snapshots with empty ACCOUNT_SETTINGS_URL', () => {
|
||||
getConfig.mockReturnValue({
|
||||
ACCOUNT_SETTINGS_URL: '',
|
||||
});
|
||||
const el = shallow(<NotificationsBanner hide />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('snapshots with ACCOUNT_SETTINGS_URL', () => {
|
||||
getConfig.mockReturnValue({
|
||||
ACCOUNT_SETTINGS_URL: 'http://localhost:1997',
|
||||
});
|
||||
const el = shallow(<NotificationsBanner hide />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
@@ -1,48 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`NotificationsBanner component snapshots with ACCOUNT_SETTINGS_URL 1`] = `
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="You can now enable notifications for ORA assignments that require staff grading, from the "
|
||||
description="user info message that user can enable notifications for ORA assignments"
|
||||
id="ora-grading.NotificationsBanner.Message"
|
||||
/>
|
||||
<Hyperlink
|
||||
destination="http://localhost:1997/#notifications"
|
||||
isInline={true}
|
||||
rel="noopener noreferrer"
|
||||
showLaunchIcon={false}
|
||||
target="_blank"
|
||||
variant="muted"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="preferences center."
|
||||
description="placeholder for the preferences center link"
|
||||
id="ora-grading.NotificationsBanner.linkMessage"
|
||||
/>
|
||||
</Hyperlink>
|
||||
</span>
|
||||
</PageBanner>
|
||||
`;
|
||||
|
||||
exports[`NotificationsBanner component snapshots with empty ACCOUNT_SETTINGS_URL 1`] = `
|
||||
<PageBanner
|
||||
variant="accentB"
|
||||
>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="You can now enable notifications for ORA assignments that require staff grading, from the "
|
||||
description="user info message that user can enable notifications for ORA assignments"
|
||||
id="ora-grading.NotificationsBanner.Message"
|
||||
/>
|
||||
<FormattedMessage
|
||||
defaultMessage="preferences center."
|
||||
description="placeholder for the preferences center link"
|
||||
id="ora-grading.NotificationsBanner.linkMessage"
|
||||
/>
|
||||
</span>
|
||||
</PageBanner>
|
||||
`;
|
||||
@@ -1,37 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner, Hyperlink } from '@openedx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const NotificationsBanner = () => (
|
||||
<PageBanner variant="accentB">
|
||||
<span>
|
||||
<FormattedMessage {...messages.infoMessage} />
|
||||
{
|
||||
(
|
||||
getConfig().ACCOUNT_SETTINGS_URL === null
|
||||
|| getConfig().ACCOUNT_SETTINGS_URL === undefined
|
||||
|| getConfig().ACCOUNT_SETTINGS_URL.trim().length === 0
|
||||
) ? (
|
||||
<FormattedMessage {...messages.notificationsBannerPreferencesCenterMessage} />
|
||||
) : (
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination={`${getConfig().ACCOUNT_SETTINGS_URL}/#notifications`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage {...messages.notificationsBannerPreferencesCenterMessage} />
|
||||
</Hyperlink>
|
||||
)
|
||||
}
|
||||
</span>
|
||||
</PageBanner>
|
||||
);
|
||||
|
||||
export default NotificationsBanner;
|
||||
@@ -1,18 +0,0 @@
|
||||
/* eslint-disable quotes */
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
infoMessage: {
|
||||
id: 'ora-grading.NotificationsBanner.Message',
|
||||
defaultMessage: 'You can now enable notifications for ORA assignments that require staff grading, from the ',
|
||||
description: 'user info message that user can enable notifications for ORA assignments',
|
||||
},
|
||||
notificationsBannerPreferencesCenterMessage: {
|
||||
id: 'ora-grading.NotificationsBanner.linkMessage',
|
||||
defaultMessage: 'preferences center.',
|
||||
description: 'placeholder for the preferences center link',
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
@@ -1,6 +1,5 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { RequestKeys, RequestStates } from 'data/constants/requests';
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import {
|
||||
@@ -9,10 +8,12 @@ import {
|
||||
FileDownload,
|
||||
statusMapping,
|
||||
} from './FileDownload';
|
||||
import messages from './messages';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
requests: { requestStatus: (...args) => ({ requestStatus: args }) },
|
||||
requests: { requestStatus: jest.fn((state, { requestKey }) => ({ status: 'inactive', requestKey })) },
|
||||
},
|
||||
thunkActions: {
|
||||
download: { downloadFiles: jest.fn() },
|
||||
@@ -20,50 +21,76 @@ jest.mock('data/redux', () => ({
|
||||
}));
|
||||
|
||||
describe('FileDownload', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
requestStatus: { status: RequestStates.inactive },
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
props.downloadFiles = jest.fn().mockName('this.props.downloadFiles');
|
||||
el = shallow(<FileDownload {...props} />);
|
||||
const defaultProps = {
|
||||
requestStatus: { status: RequestStates.inactive },
|
||||
downloadFiles: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders StatefulButton with default state when inactive', () => {
|
||||
renderWithIntl(<FileDownload {...defaultProps} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveTextContent(messages.downloadFiles.defaultMessage);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('download is inactive', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.props.state).toEqual(statusMapping[RequestStates.inactive]);
|
||||
});
|
||||
test('download is pending', () => {
|
||||
el = shallow(<FileDownload {...props} requestStatus={{ status: RequestStates.pending }} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.props.state).toEqual(statusMapping[RequestStates.pending]);
|
||||
});
|
||||
test('download is completed', () => {
|
||||
el = shallow(<FileDownload {...props} requestStatus={{ status: RequestStates.completed }} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.props.state).toEqual(statusMapping[RequestStates.completed]);
|
||||
});
|
||||
test('download is failed', () => {
|
||||
el = shallow(<FileDownload {...props} requestStatus={{ status: RequestStates.failed }} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
expect(el.instance.props.state).toEqual(statusMapping[RequestStates.failed]);
|
||||
});
|
||||
|
||||
it('renders with pending state when download is pending', () => {
|
||||
const props = { ...defaultProps, requestStatus: { status: RequestStates.pending } };
|
||||
renderWithIntl(<FileDownload {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('pgn__stateful-btn-state-pending');
|
||||
expect(button).toHaveAttribute('aria-disabled', 'true');
|
||||
expect(button).toHaveTextContent(messages.downloading.defaultMessage);
|
||||
});
|
||||
|
||||
it('renders with completed state when download is completed', () => {
|
||||
const props = { ...defaultProps, requestStatus: { status: RequestStates.completed } };
|
||||
renderWithIntl(<FileDownload {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toHaveClass('pgn__stateful-btn-state-completed');
|
||||
});
|
||||
|
||||
it('renders with failed state when download fails', () => {
|
||||
const props = { ...defaultProps, requestStatus: { status: RequestStates.failed } };
|
||||
renderWithIntl(<FileDownload {...props} />);
|
||||
const button = screen.getByRole('button');
|
||||
expect(button).toBeInTheDocument();
|
||||
expect(button).toHaveClass('pgn__stateful-btn-state-failed');
|
||||
expect(button).toHaveTextContent(messages.retryDownload.defaultMessage);
|
||||
});
|
||||
|
||||
it('calls downloadFiles when button is clicked', async () => {
|
||||
renderWithIntl(<FileDownload {...defaultProps} />);
|
||||
const user = userEvent.setup();
|
||||
const button = screen.getByRole('button');
|
||||
await user.click(button);
|
||||
expect(defaultProps.downloadFiles).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('maps request states to button states correctly', () => {
|
||||
expect(statusMapping[RequestStates.inactive]).toBe('default');
|
||||
expect(statusMapping[RequestStates.pending]).toBe('pending');
|
||||
expect(statusMapping[RequestStates.completed]).toBe('completed');
|
||||
expect(statusMapping[RequestStates.failed]).toBe('failed');
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const requestKey = RequestKeys.downloadFiles;
|
||||
const testState = { some: 'test-state' };
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('requestStatus loads from requests.requestStatus(downloadFiles)', () => {
|
||||
expect(mapped.requestStatus).toEqual(selectors.requests.requestStatus(testState, { requestKey }));
|
||||
|
||||
it('maps requestStatus from requests.requestStatus selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
const expectedResult = selectors.requests.requestStatus(testState, { requestKey: RequestKeys.downloadFiles });
|
||||
expect(mapped.requestStatus).toEqual(expectedResult);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('loads downloadFiles from thunkActions.download.downloadFiles', () => {
|
||||
it('maps downloadFiles from thunkActions', () => {
|
||||
expect(mapDispatchToProps.downloadFiles).toEqual(thunkActions.download.downloadFiles);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,56 +1,56 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { FileTypes } from 'data/constants/files';
|
||||
import { FileRenderer } from 'components/FilePreview';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { PreviewDisplay } from './PreviewDisplay';
|
||||
|
||||
jest.mock('components/FilePreview', () => ({
|
||||
FileRenderer: () => 'FileRenderer',
|
||||
}));
|
||||
|
||||
describe('PreviewDisplay', () => {
|
||||
describe('component', () => {
|
||||
const supportedTypes = Object.values(FileTypes);
|
||||
const props = {
|
||||
files: [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
name: `fake_file_${index}.${fileType}`,
|
||||
description: `file description ${index}`,
|
||||
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
|
||||
})),
|
||||
{
|
||||
name: 'bad_ext_fake_file.other',
|
||||
description: 'bad_ext file description',
|
||||
downloadUrl: 'bad_ext.other',
|
||||
},
|
||||
],
|
||||
const supportedTypes = Object.values(FileTypes);
|
||||
const props = {
|
||||
files: [
|
||||
...supportedTypes.map((fileType, index) => ({
|
||||
name: `fake_file_${index}.${fileType}`,
|
||||
description: `file description ${index}`,
|
||||
downloadUrl: `/url-path/fake_file_${index}.${fileType}`,
|
||||
})),
|
||||
{
|
||||
name: 'bad_ext_fake_file.other',
|
||||
description: 'bad_ext file description',
|
||||
downloadUrl: 'bad_ext.other',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders preview display container', () => {
|
||||
renderWithIntl(<PreviewDisplay {...props} />);
|
||||
const previewDisplay = screen.getByRole('button', { name: 'fake_file_0.pdf' });
|
||||
expect(previewDisplay).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders empty container when no files provided', () => {
|
||||
renderWithIntl(<PreviewDisplay files={[]} />);
|
||||
const previewDisplay = document.querySelector('.preview-display');
|
||||
expect(previewDisplay).toBeInTheDocument();
|
||||
expect(previewDisplay.children.length).toBe(0);
|
||||
});
|
||||
|
||||
it('only renders supported file types', () => {
|
||||
renderWithIntl(<PreviewDisplay {...props} />);
|
||||
const previewDisplay = document.querySelector('.preview-display');
|
||||
expect(previewDisplay.children.length).toBe(supportedTypes.length);
|
||||
});
|
||||
|
||||
it('filters out unsupported file types', () => {
|
||||
const unsupportedFile = {
|
||||
name: 'unsupported.xyz',
|
||||
description: 'unsupported file',
|
||||
downloadUrl: '/unsupported.xyz',
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<PreviewDisplay {...props} />);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('files render with props', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('files does not exist', () => {
|
||||
el = shallow(<PreviewDisplay {...props} files={[]} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('only renders compatible files', () => {
|
||||
const cards = el.instance.findByType(FileRenderer);
|
||||
expect(cards.length).toEqual(supportedTypes.length);
|
||||
cards.forEach((_, index) => {
|
||||
expect(
|
||||
cards[index].props.file,
|
||||
).toEqual(props.files[index]);
|
||||
});
|
||||
});
|
||||
});
|
||||
renderWithIntl(<PreviewDisplay files={[unsupportedFile]} />);
|
||||
const previewDisplay = document.querySelector('.preview-display');
|
||||
expect(previewDisplay.children.length).toBe(0);
|
||||
});
|
||||
});
|
||||
|
||||
30
src/containers/ResponseDisplay/PromptDisplay.jsx
Normal file
30
src/containers/ResponseDisplay/PromptDisplay.jsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React from 'react';
|
||||
import { Collapsible } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import messages from './messages';
|
||||
|
||||
const PromptDisplay = ({
|
||||
prompt,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const msg = intl.formatMessage(messages.promptCollapsibleHeader);
|
||||
return (
|
||||
<div className="prompt-display">
|
||||
<Collapsible
|
||||
defaultOpen
|
||||
styling="card-lg"
|
||||
title={<h3>{msg}</h3>}
|
||||
>
|
||||
{ prompt }
|
||||
</Collapsible>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
PromptDisplay.propTypes = {
|
||||
prompt: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default PromptDisplay;
|
||||
@@ -1,14 +1,20 @@
|
||||
@import "@openedx/paragon/scss/core/core";
|
||||
|
||||
.response-display {
|
||||
padding: map-get($spacers, 0);
|
||||
width: map-get($container-max-widths, "md");
|
||||
padding: var(--pgn-spacing-spacer-0);
|
||||
width: var(--pgn-size-container-max-width-md);
|
||||
overflow-y: hidden;
|
||||
height: fit-content;
|
||||
|
||||
.prompt-display-single {
|
||||
padding: var(--pgn-spacing-spacer-3) 0;
|
||||
}
|
||||
|
||||
.prompt-display-multiple > .collapsible-basic .collapsible-trigger {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.submission-files {
|
||||
.submission-files-title {
|
||||
padding: map-get($spacers, 3);
|
||||
padding: var(--pgn-spacing-spacer-3);
|
||||
border-radius: calc(0.375rem - 1px);
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 100ms ease 150ms;
|
||||
@@ -25,13 +31,13 @@
|
||||
cursor: initial;
|
||||
|
||||
> h3 {
|
||||
color: $gray-300;
|
||||
color: var(--pgn-color-gray-300);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.submission-files-body {
|
||||
padding: map-get($spacers, 3);
|
||||
padding: var(--pgn-spacing-spacer-3);
|
||||
padding-top: 0;
|
||||
|
||||
.submission-files-table thead {
|
||||
@@ -41,7 +47,11 @@
|
||||
}
|
||||
|
||||
.preview-display {
|
||||
padding: map-get($spacers, 3) 0;
|
||||
padding: var(--pgn-spacing-spacer-3) 0;
|
||||
}
|
||||
|
||||
.response-display-card {
|
||||
margin: var(--pgn-spacing-spacer-3) 0;
|
||||
}
|
||||
|
||||
.response-display-text-content {
|
||||
@@ -50,12 +60,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
@include media-breakpoint-down(sm) {
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
.response-display {
|
||||
width: 100%;
|
||||
|
||||
.preview-display {
|
||||
padding: map-get($spacers, 1) 0;
|
||||
padding: var(--pgn-spacing-spacer-1) 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import {
|
||||
Card, Collapsible, Icon, DataTable, Button,
|
||||
} from '@openedx/paragon';
|
||||
import { ArrowDropDown, ArrowDropUp, WarningFilled } from '@openedx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { downloadAllLimit, downloadSingleLimit } from 'data/constants/files';
|
||||
|
||||
@@ -19,93 +19,91 @@ import messages from './messages';
|
||||
/**
|
||||
* <SubmissionFiles />
|
||||
*/
|
||||
export class SubmissionFiles extends React.Component {
|
||||
get title() {
|
||||
return `${this.props.intl.formatMessage(messages.submissionFiles)} (${this.props.files.length})`;
|
||||
}
|
||||
export const SubmissionFiles = ({ files }) => {
|
||||
const intl = useIntl();
|
||||
|
||||
get canDownload() {
|
||||
const getTitle = () => `${intl.formatMessage(messages.submissionFiles)} (${files.length})`;
|
||||
|
||||
const getCanDownload = () => {
|
||||
let totalFileSize = 0;
|
||||
const exceedFileSize = this.props.files.some(file => {
|
||||
const exceedFileSize = files.some(file => {
|
||||
totalFileSize += file.size;
|
||||
return file.size > downloadSingleLimit;
|
||||
});
|
||||
|
||||
return !exceedFileSize && totalFileSize < downloadAllLimit;
|
||||
}
|
||||
};
|
||||
|
||||
render() {
|
||||
const { files, intl } = this.props;
|
||||
return (
|
||||
<Card className="submission-files">
|
||||
{files.length ? (
|
||||
<>
|
||||
<Collapsible.Advanced defaultOpen>
|
||||
<Collapsible.Trigger className="submission-files-title">
|
||||
<h3 data-testid="submission-files-title">{this.title}</h3>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ArrowDropDown} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ArrowDropUp} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="submission-files-body">
|
||||
<div className="submission-files-table">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tableNameHeader),
|
||||
accessor: 'name',
|
||||
Cell: FileNameCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(
|
||||
messages.tableExtensionHeader,
|
||||
),
|
||||
accessor: 'name',
|
||||
id: 'extension',
|
||||
Cell: FileExtensionCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tablePopoverHeader),
|
||||
accessor: '',
|
||||
Cell: FilePopoverCell,
|
||||
},
|
||||
]}
|
||||
data={files}
|
||||
itemCount={files.length}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
return (
|
||||
<Card className="submission-files">
|
||||
{files.length ? (
|
||||
<>
|
||||
<Collapsible.Advanced defaultOpen>
|
||||
<Collapsible.Trigger className="submission-files-title">
|
||||
<h3 data-testid="submission-files-title">{getTitle()}</h3>
|
||||
<Collapsible.Visible whenClosed>
|
||||
<Icon src={ArrowDropDown} />
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible whenOpen>
|
||||
<Icon src={ArrowDropUp} />
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body className="submission-files-body">
|
||||
<div className="submission-files-table">
|
||||
<DataTable
|
||||
columns={[
|
||||
{
|
||||
Header: intl.formatMessage(messages.tableNameHeader),
|
||||
accessor: 'name',
|
||||
Cell: FileNameCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(
|
||||
messages.tableExtensionHeader,
|
||||
),
|
||||
accessor: 'name',
|
||||
id: 'extension',
|
||||
Cell: FileExtensionCell,
|
||||
},
|
||||
{
|
||||
Header: intl.formatMessage(messages.tablePopoverHeader),
|
||||
accessor: '',
|
||||
Cell: FilePopoverCell,
|
||||
},
|
||||
]}
|
||||
data={files}
|
||||
itemCount={files.length}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer className="text-right">
|
||||
{
|
||||
getCanDownload() ? <FileDownload files={files} data-testid="file-download" /> : (
|
||||
<div>
|
||||
<Icon className="d-inline-block align-middle" src={WarningFilled} />
|
||||
<span className="exceed-download-text" data-testid="exceed-download-text"> {intl.formatMessage(messages.exceedFileSize)} </span>
|
||||
<Button disabled>{intl.formatMessage(messages.downloadFiles)}</Button>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer className="text-right">
|
||||
{
|
||||
this.canDownload ? <FileDownload files={files} data-testid="file-download" /> : (
|
||||
<div>
|
||||
<Icon className="d-inline-block align-middle" src={WarningFilled} />
|
||||
<span className="exceed-download-text" data-testid="exceed-download-text"> {intl.formatMessage(messages.exceedFileSize)} </span>
|
||||
<Button disabled>{intl.formatMessage(messages.downloadFiles)}</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
</Card.Footer>
|
||||
</>
|
||||
) : (
|
||||
<div className="submission-files-title no-submissions">
|
||||
<h3>{this.title}</h3>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
</Card.Footer>
|
||||
</>
|
||||
) : (
|
||||
<div className="submission-files-title no-submissions">
|
||||
<h3>{getTitle()}</h3>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
);
|
||||
};
|
||||
|
||||
SubmissionFiles.defaultProps = {
|
||||
files: [],
|
||||
};
|
||||
|
||||
SubmissionFiles.propTypes = {
|
||||
files: PropTypes.arrayOf(
|
||||
PropTypes.shape({
|
||||
@@ -114,7 +112,6 @@ SubmissionFiles.propTypes = {
|
||||
downloadURL: PropTypes.string,
|
||||
}),
|
||||
),
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default injectIntl(SubmissionFiles);
|
||||
export default SubmissionFiles;
|
||||
|
||||
@@ -1,99 +1,94 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { screen } from '@testing-library/react';
|
||||
import { downloadAllLimit, downloadSingleLimit } from 'data/constants/files';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { SubmissionFiles } from './SubmissionFiles';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('./components/FileNameCell', () => jest.fn().mockName('FileNameCell'));
|
||||
jest.mock('./components/FileExtensionCell', () => jest.fn().mockName('FileExtensionCell'));
|
||||
jest.mock('./components/FilePopoverCell', () => jest.fn().mockName('FilePopoverCell'));
|
||||
jest.mock('./FileDownload', () => 'FileDownload');
|
||||
jest.mock('./components/FileNameCell', () => jest.fn(({ value }) => <div>Name: {value}</div>));
|
||||
jest.mock('./components/FileExtensionCell', () => jest.fn(({ value }) => <div>Extension: {value}</div>));
|
||||
jest.mock('./components/FilePopoverCell', () => jest.fn(() => <div>Popover</div>));
|
||||
jest.mock('./FileDownload', () => jest.fn(({ files }) => <div data-testid="file-download">Download {files.length} files</div>));
|
||||
|
||||
describe('SubmissionFiles', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
files: [
|
||||
{
|
||||
name: 'some file name.jpg',
|
||||
description: 'description for the file',
|
||||
downloadURL: '/valid-url-wink-wink',
|
||||
size: 0,
|
||||
},
|
||||
{
|
||||
name: 'file number 2.jpg',
|
||||
description: 'description for this file',
|
||||
downloadURL: '/url-2',
|
||||
size: 0,
|
||||
},
|
||||
],
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} />);
|
||||
const defaultProps = {
|
||||
files: [
|
||||
{
|
||||
name: 'some file name.jpg',
|
||||
description: 'description for the file',
|
||||
downloadURL: '/valid-url-wink-wink',
|
||||
size: 100,
|
||||
},
|
||||
{
|
||||
name: 'file number 2.jpg',
|
||||
description: 'description for this file',
|
||||
downloadURL: '/url-2',
|
||||
size: 200,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
it('displays submission files title with file count', () => {
|
||||
renderWithIntl(<SubmissionFiles {...defaultProps} />);
|
||||
const title = screen.getByTestId('submission-files-title');
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent(`Submission Files (${defaultProps.files.length})`);
|
||||
});
|
||||
|
||||
describe('snapshot', () => {
|
||||
test('files existed for props', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('files does not exist', () => {
|
||||
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} files={[]} />);
|
||||
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
test('files size exceed', () => {
|
||||
const files = props.files.map(file => ({ ...file, size: downloadSingleLimit + 1 }));
|
||||
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} files={files} />);
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
it('renders file download component when files can be downloaded', () => {
|
||||
renderWithIntl(<SubmissionFiles {...defaultProps} />);
|
||||
const downloadComponent = screen.getByTestId('file-download');
|
||||
expect(downloadComponent).toBeInTheDocument();
|
||||
expect(downloadComponent).toHaveTextContent('Download 2 files');
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('title', () => {
|
||||
const titleEl = el.instance.findByTestId('submission-files-title')[0].children[0];
|
||||
expect(titleEl.el).toEqual(
|
||||
`${formatMessage(messages.submissionFiles)} (${props.files.length})`,
|
||||
);
|
||||
});
|
||||
it('displays warning when individual file exceeds size limit', () => {
|
||||
const largeFileProps = {
|
||||
...defaultProps,
|
||||
files: [
|
||||
{ ...defaultProps.files[0], size: downloadSingleLimit + 1 },
|
||||
defaultProps.files[1],
|
||||
],
|
||||
};
|
||||
renderWithIntl(<SubmissionFiles {...largeFileProps} />);
|
||||
|
||||
describe('canDownload', () => {
|
||||
test('normal file size', () => {
|
||||
expect(el.instance.findByTestId('file-download')).toHaveLength(1);
|
||||
});
|
||||
expect(screen.queryByTestId('file-download')).not.toBeInTheDocument();
|
||||
const warningText = screen.getByTestId('exceed-download-text');
|
||||
expect(warningText).toBeInTheDocument();
|
||||
expect(warningText).toHaveTextContent('Exceeded the allow download size');
|
||||
});
|
||||
|
||||
test('one of the file exceed the limit', () => {
|
||||
const oneFileExceed = [{ ...props.files[0], size: downloadSingleLimit + 1 }, props.files[1]];
|
||||
it('displays warning when total file size exceeds limit', () => {
|
||||
const largeFileSize = (downloadAllLimit + 1) / 20;
|
||||
const largeFilesProps = {
|
||||
...defaultProps,
|
||||
files: Array(20).fill({
|
||||
name: 'large file.jpg',
|
||||
description: 'large file description',
|
||||
downloadURL: '/large-file-url',
|
||||
size: largeFileSize,
|
||||
}),
|
||||
};
|
||||
renderWithIntl(<SubmissionFiles {...largeFilesProps} />);
|
||||
|
||||
oneFileExceed.forEach(file => expect(file.size < downloadAllLimit).toEqual(true));
|
||||
expect(screen.queryByTestId('file-download')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} files={oneFileExceed} />);
|
||||
expect(el.instance.findByTestId('file-download')).toHaveLength(0);
|
||||
it('displays title only when no files are provided', () => {
|
||||
renderWithIntl(<SubmissionFiles {...defaultProps} files={[]} />);
|
||||
const title = screen.getByRole('heading', { level: 3 });
|
||||
expect(title).toBeInTheDocument();
|
||||
expect(title).toHaveTextContent('Submission Files (0)');
|
||||
expect(screen.queryByTestId('file-download')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
const warningEl = el.instance.findByTestId('exceed-download-text')[0];
|
||||
expect(warningEl.el.children[1]).toEqual(formatMessage(messages.exceedFileSize));
|
||||
});
|
||||
|
||||
test('total file size exceed the limit', () => {
|
||||
const length = 20;
|
||||
const totalFilesExceed = new Array(length).fill({
|
||||
name: 'some file name.jpg',
|
||||
description: 'description for the file',
|
||||
downloadURL: '/valid-url-wink-wink',
|
||||
size: (downloadAllLimit + 1) / length,
|
||||
});
|
||||
totalFilesExceed.forEach(file => {
|
||||
expect(file.size < downloadAllLimit).toEqual(true);
|
||||
expect(file.size < downloadSingleLimit).toEqual(true);
|
||||
});
|
||||
|
||||
el = shallow(<SubmissionFiles intl={{ formatMessage }} {...props} files={totalFilesExceed} />);
|
||||
expect(el.instance.findByTestId('file-download')).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
it('renders data table with correct file information', () => {
|
||||
const { container } = renderWithIntl(<SubmissionFiles {...defaultProps} />);
|
||||
const dataTable = container.querySelector('.submission-files-table');
|
||||
expect(dataTable).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,213 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileDownload component snapshot download is completed 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
[
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
{
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
{
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files pending state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="completed"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is failed 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
[
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
{
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
{
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files pending state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="failed"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is inactive 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
[
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
{
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
{
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files pending state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="default"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`FileDownload component snapshot download is pending 1`] = `
|
||||
<StatefulButton
|
||||
disabledStates={
|
||||
[
|
||||
"pending",
|
||||
"complete",
|
||||
]
|
||||
}
|
||||
icons={
|
||||
{
|
||||
"complete": <Icon
|
||||
className="fa fa-check"
|
||||
/>,
|
||||
"default": <Icon
|
||||
className="fa fa-download"
|
||||
/>,
|
||||
"failed": <Icon
|
||||
className="fa fa-refresh"
|
||||
/>,
|
||||
"pending": <Icon
|
||||
className="fa fa-spinner fa-spin"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
labels={
|
||||
{
|
||||
"complete": <FormattedMessage
|
||||
defaultMessage="Downloaded!"
|
||||
description="Download files completed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloaded"
|
||||
/>,
|
||||
"default": <FormattedMessage
|
||||
defaultMessage="Download files"
|
||||
description="Download files inactive state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloadFiles"
|
||||
/>,
|
||||
"failed": <FormattedMessage
|
||||
defaultMessage="Retry download"
|
||||
description="Download files failed state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.retryDownload"
|
||||
/>,
|
||||
"pending": <FormattedMessage
|
||||
defaultMessage="Downloading"
|
||||
description="Download files pending state label"
|
||||
id="ora-grading.ResponseDisplay.SubmissionFiles.downloading"
|
||||
/>,
|
||||
}
|
||||
}
|
||||
onClick={[MockFunction this.props.downloadFiles]}
|
||||
state="pending"
|
||||
/>
|
||||
`;
|
||||
@@ -1,124 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`PreviewDisplay component snapshot files does not exist 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`PreviewDisplay component snapshot files render with props 1`] = `
|
||||
<div
|
||||
className="preview-display"
|
||||
>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 0",
|
||||
"downloadUrl": "/url-path/fake_file_0.pdf",
|
||||
"name": "fake_file_0.pdf",
|
||||
}
|
||||
}
|
||||
key="fake_file_0.pdf"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 1",
|
||||
"downloadUrl": "/url-path/fake_file_1.jpg",
|
||||
"name": "fake_file_1.jpg",
|
||||
}
|
||||
}
|
||||
key="fake_file_1.jpg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 2",
|
||||
"downloadUrl": "/url-path/fake_file_2.jpeg",
|
||||
"name": "fake_file_2.jpeg",
|
||||
}
|
||||
}
|
||||
key="fake_file_2.jpeg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 3",
|
||||
"downloadUrl": "/url-path/fake_file_3.png",
|
||||
"name": "fake_file_3.png",
|
||||
}
|
||||
}
|
||||
key="fake_file_3.png"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 4",
|
||||
"downloadUrl": "/url-path/fake_file_4.bmp",
|
||||
"name": "fake_file_4.bmp",
|
||||
}
|
||||
}
|
||||
key="fake_file_4.bmp"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 5",
|
||||
"downloadUrl": "/url-path/fake_file_5.txt",
|
||||
"name": "fake_file_5.txt",
|
||||
}
|
||||
}
|
||||
key="fake_file_5.txt"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 6",
|
||||
"downloadUrl": "/url-path/fake_file_6.gif",
|
||||
"name": "fake_file_6.gif",
|
||||
}
|
||||
}
|
||||
key="fake_file_6.gif"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 7",
|
||||
"downloadUrl": "/url-path/fake_file_7.jfif",
|
||||
"name": "fake_file_7.jfif",
|
||||
}
|
||||
}
|
||||
key="fake_file_7.jfif"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 8",
|
||||
"downloadUrl": "/url-path/fake_file_8.pjpeg",
|
||||
"name": "fake_file_8.pjpeg",
|
||||
}
|
||||
}
|
||||
key="fake_file_8.pjpeg"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 9",
|
||||
"downloadUrl": "/url-path/fake_file_9.pjp",
|
||||
"name": "fake_file_9.pjp",
|
||||
}
|
||||
}
|
||||
key="fake_file_9.pjp"
|
||||
/>
|
||||
<FileRenderer
|
||||
file={
|
||||
{
|
||||
"description": "file description 10",
|
||||
"downloadUrl": "/url-path/fake_file_10.svg",
|
||||
"name": "fake_file_10.svg",
|
||||
}
|
||||
}
|
||||
key="fake_file_10.svg"
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,230 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`SubmissionFiles component snapshot files does not exist 1`] = `
|
||||
<Card
|
||||
className="submission-files"
|
||||
>
|
||||
<div
|
||||
className="submission-files-title no-submissions"
|
||||
>
|
||||
<h3>
|
||||
Submission Files (0)
|
||||
</h3>
|
||||
</div>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
exports[`SubmissionFiles component snapshot files existed for props 1`] = `
|
||||
<Card
|
||||
className="submission-files"
|
||||
>
|
||||
<Fragment>
|
||||
<Collapsible.Advanced
|
||||
defaultOpen={true}
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
className="submission-files-title"
|
||||
>
|
||||
<h3
|
||||
data-testid="submission-files-title"
|
||||
>
|
||||
Submission Files (2)
|
||||
</h3>
|
||||
<Collapsible.Visible
|
||||
whenClosed={true}
|
||||
>
|
||||
<Icon
|
||||
src={[MockFunction icons.ArrowDropDown]}
|
||||
/>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible
|
||||
whenOpen={true}
|
||||
>
|
||||
<Icon
|
||||
src={[MockFunction icons.ArrowDropUp]}
|
||||
/>
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body
|
||||
className="submission-files-body"
|
||||
>
|
||||
<div
|
||||
className="submission-files-table"
|
||||
>
|
||||
<DataTable
|
||||
columns={
|
||||
[
|
||||
{
|
||||
"Cell": [MockFunction FileNameCell],
|
||||
"Header": "Name",
|
||||
"accessor": "name",
|
||||
},
|
||||
{
|
||||
"Cell": [MockFunction FileExtensionCell],
|
||||
"Header": "File Extension",
|
||||
"accessor": "name",
|
||||
"id": "extension",
|
||||
},
|
||||
{
|
||||
"Cell": [MockFunction FilePopoverCell],
|
||||
"Header": "File Metadata",
|
||||
"accessor": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
[
|
||||
{
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
"size": 0,
|
||||
},
|
||||
{
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
"size": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer
|
||||
className="text-right"
|
||||
>
|
||||
<FileDownload
|
||||
data-testid="file-download"
|
||||
files={
|
||||
[
|
||||
{
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
"size": 0,
|
||||
},
|
||||
{
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
"size": 0,
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
</Card.Footer>
|
||||
</Fragment>
|
||||
</Card>
|
||||
`;
|
||||
|
||||
exports[`SubmissionFiles component snapshot files size exceed 1`] = `
|
||||
<Card
|
||||
className="submission-files"
|
||||
>
|
||||
<Fragment>
|
||||
<Collapsible.Advanced
|
||||
defaultOpen={true}
|
||||
>
|
||||
<Collapsible.Trigger
|
||||
className="submission-files-title"
|
||||
>
|
||||
<h3
|
||||
data-testid="submission-files-title"
|
||||
>
|
||||
Submission Files (2)
|
||||
</h3>
|
||||
<Collapsible.Visible
|
||||
whenClosed={true}
|
||||
>
|
||||
<Icon
|
||||
src={[MockFunction icons.ArrowDropDown]}
|
||||
/>
|
||||
</Collapsible.Visible>
|
||||
<Collapsible.Visible
|
||||
whenOpen={true}
|
||||
>
|
||||
<Icon
|
||||
src={[MockFunction icons.ArrowDropUp]}
|
||||
/>
|
||||
</Collapsible.Visible>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Body
|
||||
className="submission-files-body"
|
||||
>
|
||||
<div
|
||||
className="submission-files-table"
|
||||
>
|
||||
<DataTable
|
||||
columns={
|
||||
[
|
||||
{
|
||||
"Cell": [MockFunction FileNameCell],
|
||||
"Header": "Name",
|
||||
"accessor": "name",
|
||||
},
|
||||
{
|
||||
"Cell": [MockFunction FileExtensionCell],
|
||||
"Header": "File Extension",
|
||||
"accessor": "name",
|
||||
"id": "extension",
|
||||
},
|
||||
{
|
||||
"Cell": [MockFunction FilePopoverCell],
|
||||
"Header": "File Metadata",
|
||||
"accessor": "",
|
||||
},
|
||||
]
|
||||
}
|
||||
data={
|
||||
[
|
||||
{
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
"size": 1610612737,
|
||||
},
|
||||
{
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
"size": 1610612737,
|
||||
},
|
||||
]
|
||||
}
|
||||
itemCount={2}
|
||||
>
|
||||
<DataTable.Table />
|
||||
</DataTable>
|
||||
</div>
|
||||
</Collapsible.Body>
|
||||
</Collapsible.Advanced>
|
||||
<Card.Footer
|
||||
className="text-right"
|
||||
>
|
||||
<div>
|
||||
<Icon
|
||||
className="d-inline-block align-middle"
|
||||
/>
|
||||
<span
|
||||
className="exceed-download-text"
|
||||
data-testid="exceed-download-text"
|
||||
>
|
||||
|
||||
Exceeded the allow download size
|
||||
|
||||
</span>
|
||||
<Button
|
||||
disabled={true}
|
||||
>
|
||||
Download files
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Footer>
|
||||
</Fragment>
|
||||
</Card>
|
||||
`;
|
||||
@@ -1,99 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`ResponseDisplay component snapshot file upload disable with valid response 1`] = `
|
||||
<div
|
||||
className="response-display"
|
||||
>
|
||||
<Card
|
||||
key="0"
|
||||
>
|
||||
<Card.Section
|
||||
className="response-display-text-content"
|
||||
data-testid="response-display-text-content"
|
||||
>
|
||||
parsed html (sanitized (some text response here))
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResponseDisplay component snapshot file upload disabled without response 1`] = `
|
||||
<div
|
||||
className="response-display"
|
||||
>
|
||||
<SubmissionFiles
|
||||
data-testid="submission-files"
|
||||
files={[]}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
data-testid="allow-file-upload"
|
||||
files={[]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResponseDisplay component snapshot file upload enable with valid response 1`] = `
|
||||
<div
|
||||
className="response-display"
|
||||
>
|
||||
<SubmissionFiles
|
||||
data-testid="submission-files"
|
||||
files={
|
||||
[
|
||||
{
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
},
|
||||
{
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
data-testid="allow-file-upload"
|
||||
files={
|
||||
[
|
||||
{
|
||||
"description": "description for the file",
|
||||
"downloadURL": "/valid-url-wink-wink",
|
||||
"name": "some file name.jpg",
|
||||
},
|
||||
{
|
||||
"description": "description for this file",
|
||||
"downloadURL": "/url-2",
|
||||
"name": "file number 2.jpg",
|
||||
},
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Card
|
||||
key="0"
|
||||
>
|
||||
<Card.Section
|
||||
className="response-display-text-content"
|
||||
data-testid="response-display-text-content"
|
||||
>
|
||||
parsed html (sanitized (some text response here))
|
||||
</Card.Section>
|
||||
</Card>
|
||||
</div>
|
||||
`;
|
||||
|
||||
exports[`ResponseDisplay component snapshot file upload enable without response 1`] = `
|
||||
<div
|
||||
className="response-display"
|
||||
>
|
||||
<SubmissionFiles
|
||||
data-testid="submission-files"
|
||||
files={[]}
|
||||
/>
|
||||
<PreviewDisplay
|
||||
data-testid="allow-file-upload"
|
||||
files={[]}
|
||||
/>
|
||||
</div>
|
||||
`;
|
||||
@@ -1,25 +1,39 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import FileExtensionCell from './FileExtensionCell';
|
||||
|
||||
describe('FileExtensionCell', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
value: 'file_name.with_extension.pdf',
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileExtensionCell {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
const props = {
|
||||
value: 'file_name.with_extension.pdf',
|
||||
};
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
expect(el.instance.children[0].el).toEqual('PDF');
|
||||
});
|
||||
});
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('renders file extension in uppercase', () => {
|
||||
render(<FileExtensionCell {...props} />);
|
||||
expect(screen.getByText('PDF')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies correct CSS class', () => {
|
||||
const { container } = render(<FileExtensionCell {...props} />);
|
||||
const element = container.firstChild;
|
||||
expect(element).toHaveClass('text-truncate');
|
||||
});
|
||||
|
||||
it('extracts extension from file with multiple dots', () => {
|
||||
render(<FileExtensionCell value="my.file.name.docx" />);
|
||||
expect(screen.getByText('DOCX')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles file without extension', () => {
|
||||
render(<FileExtensionCell value="filename" />);
|
||||
expect(screen.getByText('FILENAME')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty file extension', () => {
|
||||
const { container } = render(<FileExtensionCell value="filename." />);
|
||||
const element = container.firstChild;
|
||||
expect(element).toHaveTextContent('');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,21 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
|
||||
import FileNameCell from './FileNameCell';
|
||||
|
||||
describe('FileNameCell', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
value: 'some test text value',
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileNameCell {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
const props = {
|
||||
value: 'some test text value',
|
||||
};
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
expect(el.instance.children[0].el).toEqual(props.value);
|
||||
});
|
||||
});
|
||||
it('renders the value text', () => {
|
||||
render(<FileNameCell {...props} />);
|
||||
expect(screen.getByText('some test text value')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('applies text truncation class', () => {
|
||||
const { container } = render(<FileNameCell {...props} />);
|
||||
const divElement = container.querySelector('div');
|
||||
expect(divElement).toHaveClass('text-truncate');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,37 +1,39 @@
|
||||
import React from 'react';
|
||||
import { shallow } from '@edx/react-unit-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../../testUtils';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FilePopoverCell from './FilePopoverCell';
|
||||
|
||||
jest.mock('components/InfoPopover', () => 'InfoPopover');
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
|
||||
describe('FilePopoverCell', () => {
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
const props = {
|
||||
row: {
|
||||
original: {
|
||||
name: 'some file name',
|
||||
description: 'long descriptive text...',
|
||||
downloadURL: 'this-url-is.working',
|
||||
size: 1024,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('renders info button has correct alt text', () => {
|
||||
renderWithIntl(<FilePopoverCell {...props} />);
|
||||
const button = screen.getByRole('button', { name: /display more info/i });
|
||||
expect(button).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles empty row.original object', () => {
|
||||
const emptyProps = {
|
||||
row: {
|
||||
original: {
|
||||
name: 'some file name',
|
||||
description: 'long descriptive text...',
|
||||
downloadURL: 'this-url-is.working',
|
||||
},
|
||||
original: {},
|
||||
},
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverCell {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el.snapshot).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
test('content', () => {
|
||||
const { original } = props.row;
|
||||
const content = el.instance.findByType(FilePopoverContent)[0];
|
||||
expect(content.props).toEqual({ ...original });
|
||||
});
|
||||
});
|
||||
const { container } = renderWithIntl(<FilePopoverCell {...emptyProps} />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('handles missing row prop', () => {
|
||||
const { container } = renderWithIntl(<FilePopoverCell />);
|
||||
expect(container.firstChild).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileExtensionCell component snapshot 1`] = `
|
||||
<div
|
||||
className="text-truncate"
|
||||
>
|
||||
PDF
|
||||
</div>
|
||||
`;
|
||||
@@ -1,9 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileNameCell component snapshot 1`] = `
|
||||
<div
|
||||
className="text-truncate"
|
||||
>
|
||||
some test text value
|
||||
</div>
|
||||
`;
|
||||
@@ -1,11 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FilePopoverCell component snapshot 1`] = `
|
||||
<InfoPopover>
|
||||
<FilePopoverContent
|
||||
description="long descriptive text..."
|
||||
downloadURL="this-url-is.working"
|
||||
name="some file name"
|
||||
/>
|
||||
</InfoPopover>
|
||||
`;
|
||||
@@ -11,9 +11,10 @@ import parse from 'html-react-parser';
|
||||
import { selectors } from 'data/redux';
|
||||
import { fileUploadResponseOptions } from 'data/services/lms/constants';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import SubmissionFiles from './SubmissionFiles';
|
||||
import PreviewDisplay from './PreviewDisplay';
|
||||
|
||||
import PromptDisplay from './PromptDisplay';
|
||||
import './ResponseDisplay.scss';
|
||||
|
||||
/**
|
||||
@@ -25,8 +26,14 @@ export class ResponseDisplay extends React.Component {
|
||||
this.purify = createDOMPurify(window);
|
||||
}
|
||||
|
||||
get prompts() {
|
||||
return this.props.prompts.map((item) => this.formattedHtml(item));
|
||||
}
|
||||
|
||||
get textContents() {
|
||||
return this.props.response.text.map(text => parse(this.purify.sanitize(text)));
|
||||
const { text } = this.props.response;
|
||||
const formattedText = text.map((item) => this.formattedHtml(item));
|
||||
return formattedText;
|
||||
}
|
||||
|
||||
get submittedFiles() {
|
||||
@@ -39,17 +46,28 @@ export class ResponseDisplay extends React.Component {
|
||||
);
|
||||
}
|
||||
|
||||
formattedHtml(text) {
|
||||
const cleanedText = text.replaceAll(/\.\.\/asset/g, `${getConfig().LMS_BASE_URL}/asset`);
|
||||
return parse(this.purify.sanitize(cleanedText));
|
||||
}
|
||||
|
||||
render() {
|
||||
const { prompts } = this;
|
||||
const multiPrompt = prompts.length > 1;
|
||||
return (
|
||||
<div className="response-display">
|
||||
{!multiPrompt && <PromptDisplay prompt={prompts[0]} />}
|
||||
{this.allowFileUpload && <SubmissionFiles files={this.submittedFiles} data-testid="submission-files" />}
|
||||
{this.allowFileUpload && <PreviewDisplay files={this.submittedFiles} data-testid="allow-file-upload" />}
|
||||
{
|
||||
/* eslint-disable react/no-array-index-key */
|
||||
this.textContents.map((textContent, index) => (
|
||||
<Card key={index}>
|
||||
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
|
||||
</Card>
|
||||
<>
|
||||
{ multiPrompt && <PromptDisplay prompt={prompts[index]} /> }
|
||||
<Card className="response-display-card" key={index}>
|
||||
<Card.Section className="response-display-text-content" data-testid="response-display-text-content">{textContent}</Card.Section>
|
||||
</Card>
|
||||
</>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
@@ -64,6 +82,7 @@ ResponseDisplay.defaultProps = {
|
||||
},
|
||||
fileUploadResponseConfig: fileUploadResponseOptions.none,
|
||||
};
|
||||
|
||||
ResponseDisplay.propTypes = {
|
||||
response: PropTypes.shape({
|
||||
text: PropTypes.arrayOf(PropTypes.string),
|
||||
@@ -76,11 +95,13 @@ ResponseDisplay.propTypes = {
|
||||
fileUploadResponseConfig: PropTypes.oneOf(
|
||||
Object.values(fileUploadResponseOptions),
|
||||
),
|
||||
prompts: PropTypes.arrayOf(PropTypes.string).isRequired,
|
||||
};
|
||||
|
||||
export const mapStateToProps = (state) => ({
|
||||
response: selectors.grading.selected.response(state),
|
||||
fileUploadResponseConfig: selectors.app.ora.fileUploadResponseConfig(state),
|
||||
prompts: selectors.app.ora.prompts(state),
|
||||
});
|
||||
|
||||
export const mapDispatchToProps = {};
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user