Compare commits
9 Commits
master
...
bilalqamar
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1b0a5c9abb | ||
|
|
b7e4419c3a | ||
|
|
fca889e12c | ||
|
|
edf6529704 | ||
|
|
658fb7dfe3 | ||
|
|
e7ec4f4543 | ||
|
|
5ec968653a | ||
|
|
e09d40d8b2 | ||
|
|
7572e738c9 |
3
.env
3
.env
@@ -32,6 +32,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN=''
|
||||
ENTERPRISE_MARKETING_FOOTER_UTM_MEDIUM=''
|
||||
APP_ID=''
|
||||
MFE_CONFIG_API_URL=''
|
||||
ACCOUNT_SETTINGS_URL=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
|
||||
@@ -7,6 +7,7 @@ LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -37,6 +38,3 @@ ENTERPRISE_MARKETING_UTM_CAMPAIGN='example.com Referral'
|
||||
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={}
|
||||
|
||||
@@ -7,6 +7,7 @@ LOGOUT_URL='http://localhost:18000/logout'
|
||||
LOGO_URL=https://edx-cdn.org/v3/default/logo.svg
|
||||
LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg
|
||||
LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg
|
||||
LOGO_POWERED_BY_OPEN_EDX_URL_SVG=https://edx-cdn.org/v3/stage/open-edx-tag.svg
|
||||
FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico
|
||||
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
|
||||
REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh'
|
||||
@@ -35,5 +36,3 @@ ENTERPRISE_MARKETING_URL='http://example.com'
|
||||
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={}
|
||||
|
||||
21
.eslintrc.js
21
.eslintrc.js
@@ -1,5 +1,4 @@
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
const config = createConfig('eslint', {
|
||||
rules: {
|
||||
@@ -7,28 +6,20 @@ 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
Normal file
6
.github/CODEOWNERS
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
# 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
|
||||
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -10,18 +10,17 @@ on:
|
||||
|
||||
jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- name: Setup Nodejs Env
|
||||
run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
|
||||
- name: Setup Nodejs
|
||||
uses: actions/setup-node@v4
|
||||
uses: actions/setup-node@v3
|
||||
# Because of node 18 bug (https://github.com/nodejs/node/issues/47563), Pinning node version 18.15 until the next release of node
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
node-version: 18.15
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
@@ -39,10 +38,7 @@ jobs:
|
||||
run: npm run build
|
||||
|
||||
- name: Run Coverage
|
||||
uses: codecov/codecov-action@v4
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
fail_ci_if_error: true
|
||||
uses: codecov/codecov-action@v3
|
||||
|
||||
- name: Send failure notification
|
||||
if: ${{ failure() }}
|
||||
|
||||
32
.github/workflows/npm-publish.yml
vendored
Normal file
32
.github/workflows/npm-publish.yml
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
name: Release CI
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- "*"
|
||||
|
||||
jobs:
|
||||
release:
|
||||
name: Release
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v2
|
||||
with:
|
||||
node-version: 12
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm ci
|
||||
|
||||
- name: Create Build
|
||||
run: npm run build
|
||||
|
||||
- name: Release Package
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.SEMANTIC_RELEASE_GITHUB_TOKEN }}
|
||||
NPM_TOKEN: ${{ secrets.SEMANTIC_RELEASE_NPM_TOKEN }}
|
||||
run: npm semantic-release
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -25,5 +25,3 @@ module.config.js
|
||||
### transifex ###
|
||||
src/i18n/transifex_input.json
|
||||
temp
|
||||
|
||||
src/i18n/messages
|
||||
4
.husky/pre-push
Executable file
4
.husky/pre-push
Executable file
@@ -0,0 +1,4 @@
|
||||
#!/bin/sh
|
||||
. "$(dirname "$0")/_/husky.sh"
|
||||
|
||||
npm run lint
|
||||
27
.releaserc
Normal file
27
.releaserc
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"branch": "master",
|
||||
"tagFormat": "v${version}",
|
||||
"verifyConditions": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"analyzeCommits": "@semantic-release/commit-analyzer",
|
||||
"generateNotes": "@semantic-release/release-notes-generator",
|
||||
"prepare": "@semantic-release/npm",
|
||||
"publish": [
|
||||
"@semantic-release/npm",
|
||||
{
|
||||
"path": "@semantic-release/github",
|
||||
"assets": {
|
||||
"path": "dist/*"
|
||||
}
|
||||
}
|
||||
],
|
||||
"success": [],
|
||||
"fail": []
|
||||
}
|
||||
9
.tx/config
Normal file
9
.tx/config
Normal file
@@ -0,0 +1,9 @@
|
||||
[main]
|
||||
host = https://www.transifex.com
|
||||
|
||||
[o:open-edx:p:edx-platform:r:frontend-app-ora-grading]
|
||||
file_filter = src/i18n/messages/<lang>.json
|
||||
source_file = src/i18n/transifex_input.json
|
||||
source_lang = en
|
||||
type = KEYVALUEJSON
|
||||
|
||||
28
Makefile
28
Makefile
@@ -2,13 +2,16 @@ npm-install-%: ## install specified % npm package
|
||||
npm install $* --save-dev
|
||||
git add package.json
|
||||
|
||||
transifex_resource = frontend-app-ora-grading
|
||||
transifex_langs = "ar,de_DE,es_419,fa_IR,fr,fr_CA,hi,it_IT,pt_PT,uk,ru,zh_CN"
|
||||
|
||||
intl_imports = ./node_modules/.bin/intl-imports.js
|
||||
transifex_utils = ./node_modules/.bin/transifex-utils.js
|
||||
i18n = ./src/i18n
|
||||
transifex_input = $(i18n)/transifex_input.json
|
||||
|
||||
# This directory must match .babelrc .
|
||||
transifex_temp = ./temp/babel-plugin-formatjs
|
||||
transifex_temp = ./temp/babel-plugin-react-intl
|
||||
|
||||
NPM_TESTS=build i18n_extract lint test
|
||||
|
||||
@@ -40,18 +43,35 @@ detect_changed_source_translations:
|
||||
# Checking for changed translations...
|
||||
git diff --exit-code $(i18n)
|
||||
|
||||
# Pushes translations to Transifex. You must run make extract_translations first.
|
||||
push_translations:
|
||||
# Pushing strings to Transifex...
|
||||
tx push -s
|
||||
# Fetching hashes from Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/get_hashed_strings_v3.sh
|
||||
# Writing out comments to file...
|
||||
$(transifex_utils) $(transifex_temp) --comments --v3-scripts-path
|
||||
# Pushing comments to Transifex...
|
||||
./node_modules/@edx/reactifex/bash_scripts/put_comments_v3.sh
|
||||
|
||||
ifeq ($(OPENEDX_ATLAS_PULL),)
|
||||
# Pulls translations from Transifex.
|
||||
pull_translations:
|
||||
tx pull -t -f --mode reviewed --languages=$(transifex_langs)
|
||||
else
|
||||
# Experimental: OEP-58 Pulls translations using atlas
|
||||
pull_translations:
|
||||
rm -rf src/i18n/messages
|
||||
mkdir src/i18n/messages
|
||||
cd src/i18n/messages \
|
||||
&& atlas pull $(ATLAS_OPTIONS) \
|
||||
&& atlas pull --filter=$(transifex_langs) \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-platform/src/i18n/messages:frontend-platform \
|
||||
translations/paragon/src/i18n/messages:paragon \
|
||||
translations/frontend-app-ora-grading/src/i18n/messages:frontend-app-ora-grading
|
||||
|
||||
$(intl_imports) frontend-component-footer frontend-component-header frontend-platform paragon frontend-app-ora-grading
|
||||
$(intl_imports) frontend-component-footer frontend-component-header paragon frontend-app-ora-grading
|
||||
endif
|
||||
|
||||
# This target is used by CI.
|
||||
validate-no-uncommitted-package-lock-changes:
|
||||
|
||||
22
README.rst
22
README.rst
@@ -26,20 +26,18 @@ Getting Started
|
||||
Prerequisites
|
||||
=============
|
||||
|
||||
`Tutor`_ is currently recommended as a development environment for your
|
||||
new MFE. Please refer
|
||||
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
|
||||
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?tab=readme-ov-file#mfe-development
|
||||
|
||||
|
||||
Plugins
|
||||
=======
|
||||
This MFE can be customized using `Frontend Plugin Framework <https://github.com/openedx/frontend-plugin-framework>`_.
|
||||
|
||||
The parts of this MFE that can be customized in that manner are documented `here </src/plugin-slots>`_.
|
||||
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
|
||||
|
||||
Developing
|
||||
==========
|
||||
@@ -56,9 +54,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 the version of Node specified in the ``.nvmrc`` file.
|
||||
2. Use node v18.x.
|
||||
|
||||
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
|
||||
The current version of the micro-frontend build scripts support node 18.
|
||||
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>`_.
|
||||
|
||||
@@ -13,9 +13,7 @@ metadata:
|
||||
- url: "https://ora-grading.stage.edx.org"
|
||||
title: "Stage Site"
|
||||
icon: "Web"
|
||||
annotations:
|
||||
openedx.org/release: "master"
|
||||
spec:
|
||||
owner: "user:codewithemad"
|
||||
owner: group:content-aurora
|
||||
type: 'website'
|
||||
lifecycle: 'production'
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
const { createConfig } = require('@openedx/frontend-build');
|
||||
const { createConfig } = require('@edx/frontend-build');
|
||||
|
||||
module.exports = createConfig('jest', {
|
||||
setupFilesAfterEnv: [
|
||||
@@ -6,6 +6,9 @@ module.exports = createConfig('jest', {
|
||||
'<rootDir>/src/setupTest.js',
|
||||
],
|
||||
modulePaths: ['<rootDir>/src/'],
|
||||
snapshotSerializers: [
|
||||
'enzyme-to-json/serializer',
|
||||
],
|
||||
coveragePathIgnorePatterns: [
|
||||
'src/segment.js',
|
||||
'src/postcss.config.js',
|
||||
|
||||
9
openedx.yaml
Normal file
9
openedx.yaml
Normal file
@@ -0,0 +1,9 @@
|
||||
# This file describes this Open edX repo, as described in OEP-2:
|
||||
# http://open-edx-proposals.readthedocs.io/en/latest/oeps/oep-0002.html#specification
|
||||
|
||||
tags:
|
||||
- frontend-app
|
||||
- masters
|
||||
oeps:
|
||||
oep-2: true # Repository metadata
|
||||
openedx-release: {ref: master}
|
||||
26751
package-lock.json
generated
26751
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
48
package.json
48
package.json
@@ -6,19 +6,16 @@
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/edx/frontend-app-ora-grading.git"
|
||||
},
|
||||
"browserslist": [
|
||||
"extends @edx/browserslist-config"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "fedx-scripts webpack",
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
|
||||
"lint": "fedx-scripts eslint --ext .jsx,.js src/",
|
||||
"lint-fix": "fedx-scripts eslint --fix --ext .jsx,.js src/",
|
||||
"semantic-release": "semantic-release",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"dev": "PUBLIC_PATH=/ora-grading/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io",
|
||||
"test": "TZ=GMT fedx-scripts jest --coverage --passWithNoTests",
|
||||
"watch-tests": "jest --watch"
|
||||
"watch-tests": "jest --watch",
|
||||
"prepare": "husky install"
|
||||
},
|
||||
"author": "edX",
|
||||
"license": "AGPL-3.0",
|
||||
@@ -28,25 +25,25 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/openedx-atlas": "^0.6.0",
|
||||
"@edx/frontend-component-footer": "12.7.0",
|
||||
"@edx/frontend-component-header": "4.11.1",
|
||||
"@edx/frontend-platform": "6.2.0",
|
||||
"@edx/paragon": "21.5.6",
|
||||
"@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": "^23.4.5",
|
||||
"@redux-beacon/segment": "^1.1.0",
|
||||
"@redux-devtools/extension": "3.0.0",
|
||||
"@reduxjs/toolkit": "^1.6.1",
|
||||
"@testing-library/user-event": "^14.0.0",
|
||||
"@zip.js/zip.js": "^2.4.6",
|
||||
"axios": "^0.28.0",
|
||||
"axios": "^0.27.0",
|
||||
"classnames": "^2.3.1",
|
||||
"core-js": "3.35.1",
|
||||
"dompurify": "^2.3.1",
|
||||
"email-prop-type": "^3.0.1",
|
||||
"enzyme": "^3.11.0",
|
||||
"enzyme-to-json": "^3.6.2",
|
||||
"file-saver": "^2.0.5",
|
||||
"filesize": "^8.0.6",
|
||||
"font-awesome": "4.7.0",
|
||||
@@ -56,17 +53,18 @@
|
||||
"moment": "^2.29.3",
|
||||
"prop-types": "15.8.1",
|
||||
"query-string": "7.1.3",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-helmet": "^6.1.0",
|
||||
"react-intl": "6.4.7",
|
||||
"react-pdf": "^7.0.0",
|
||||
"react-pdf": "^5.5.0",
|
||||
"react-redux": "^7.2.9",
|
||||
"react-router": "6.21.3",
|
||||
"react-router-dom": "6.21.3",
|
||||
"react-router-redux": "^5.0.0-alpha.9",
|
||||
"redux": "4.2.1",
|
||||
"redux-beacon": "^2.1.0",
|
||||
"redux-devtools-extension": "2.13.9",
|
||||
"redux-logger": "3.0.6",
|
||||
"redux-thunk": "2.4.2",
|
||||
"regenerator-runtime": "^0.14.0",
|
||||
@@ -75,18 +73,22 @@
|
||||
"whatwg-fetch": "^3.6.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/browserslist-config": "^1.3.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@edx/browserslist-config": "^1.2.0",
|
||||
"@edx/frontend-build": "13.0.10",
|
||||
"@edx/reactifex": "^2.1.1",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "12.1.5",
|
||||
"@wojtekmaj/enzyme-adapter-react-17": "0.8.0",
|
||||
"axios-mock-adapter": "^1.20.0",
|
||||
"fetch-mock": "^9.11.0",
|
||||
"husky": "^7.0.0",
|
||||
"identity-obj-proxy": "^3.0.0",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"jest-expect-message": "^1.0.2",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^18.3.1",
|
||||
"redux-mock-store": "^1.5.5"
|
||||
"react-test-renderer": "^17.0.2",
|
||||
"reactifex": "1.1.1",
|
||||
"redux-mock-store": "^1.5.4",
|
||||
"semantic-release": "^19.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,12 +3,13 @@ import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import { BrowserRouter as Router } from 'react-router-dom';
|
||||
|
||||
import { FooterSlot } from '@edx/frontend-component-footer';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
import DemoWarning from 'containers/DemoWarning';
|
||||
import CTA from 'containers/CTA';
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
import './App.scss';
|
||||
@@ -22,13 +23,13 @@ export const App = ({ courseMetadata, isEnabled }) => (
|
||||
courseTitle={courseMetadata.title}
|
||||
courseNumber={courseMetadata.number}
|
||||
courseOrg={courseMetadata.org}
|
||||
data-testid="header"
|
||||
/>
|
||||
{!isEnabled && <DemoWarning />}
|
||||
<main data-testid="main">
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<FooterSlot />
|
||||
<Footer logo={process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG} />
|
||||
</div>
|
||||
</Router>
|
||||
);
|
||||
|
||||
23
src/App.scss
23
src/App.scss
@@ -1,13 +1,15 @@
|
||||
// frontend-app-*/src/index.scss
|
||||
@use "@openedx/paragon/styles/css/core/custom-media-breakpoints" as paragonCustomMediaBreakpoints;
|
||||
@import "~@edx/brand/paragon/fonts";
|
||||
@import "~@edx/brand/paragon/variables";
|
||||
@import "~@edx/paragon/scss/core/core";
|
||||
@import "~@edx/brand/paragon/overrides";
|
||||
|
||||
$fa-font-path: "~font-awesome/fonts";
|
||||
@import "~font-awesome/scss/font-awesome";
|
||||
|
||||
$input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get upgrade to paragon 4.0.0 to work
|
||||
$input-focus-box-shadow: $input-box-shadow; // 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";
|
||||
|
||||
#root {
|
||||
display: flex;
|
||||
@@ -46,22 +48,7 @@ $input-focus-box-shadow: var(--pgn-elevation-form-input-base); // hack to get up
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
140
src/App.test.jsx
140
src/App.test.jsx
@@ -1,101 +1,77 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { selectors } from 'data/redux';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { renderWithIntl } from './testUtils';
|
||||
import { App, mapStateToProps } from './App';
|
||||
import Footer from '@edx/frontend-component-footer';
|
||||
import { LearningHeader as Header } from '@edx/frontend-component-header';
|
||||
|
||||
// we want to scope these tests to the App component, so we mock some child components to reduce complexity
|
||||
import ListView from 'containers/ListView';
|
||||
|
||||
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>
|
||||
),
|
||||
}));
|
||||
import { App } from './App';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
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)),
|
||||
app: {
|
||||
selectors: {
|
||||
courseMetadata: (state) => ({ courseMetadata: state }),
|
||||
isEnabled: (state) => ({ isEnabled: state }),
|
||||
},
|
||||
},
|
||||
}));
|
||||
|
||||
describe('App component', () => {
|
||||
const defaultProps = {
|
||||
jest.mock('@edx/frontend-component-header', () => ({
|
||||
LearningHeader: 'Header',
|
||||
}));
|
||||
jest.mock('@edx/frontend-component-footer', () => 'Footer');
|
||||
|
||||
jest.mock('containers/DemoWarning', () => 'DemoWarning');
|
||||
jest.mock('containers/CTA', () => 'CTA');
|
||||
jest.mock('containers/ListView', () => 'ListView');
|
||||
jest.mock('components/Head', () => 'Head');
|
||||
|
||||
const logo = 'fakeLogo.png';
|
||||
let el;
|
||||
let router;
|
||||
|
||||
describe('App router component', () => {
|
||||
const props = {
|
||||
courseMetadata: {
|
||||
org: 'test-org',
|
||||
number: 'test-101',
|
||||
title: 'Test Course',
|
||||
org: 'course-org',
|
||||
number: 'course-number',
|
||||
title: 'course-title',
|
||||
},
|
||||
isEnabled: true,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
test('snapshot: enabled', () => {
|
||||
expect(shallow(<App {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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();
|
||||
test('snapshot: disabled (show demo warning)', () => {
|
||||
expect(shallow(<App {...props} isEnabled={false} />)).toMatchSnapshot();
|
||||
});
|
||||
describe('component', () => {
|
||||
beforeEach(() => {
|
||||
process.env.LOGO_POWERED_BY_OPEN_EDX_URL_SVG = logo;
|
||||
el = shallow(<App {...props} />);
|
||||
router = el.childAt(0);
|
||||
});
|
||||
describe('Router', () => {
|
||||
test('Routing - ListView is only route', () => {
|
||||
expect(router.find('main')).toEqual(shallow(
|
||||
<main><ListView /></main>,
|
||||
));
|
||||
});
|
||||
});
|
||||
test('Footer logo drawn from env variable', () => {
|
||||
expect(router.find(Footer).props().logo).toEqual(logo);
|
||||
});
|
||||
|
||||
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));
|
||||
test('Header to use courseMetadata props', () => {
|
||||
const {
|
||||
courseTitle,
|
||||
courseNumber,
|
||||
courseOrg,
|
||||
} = router.find(Header).props();
|
||||
expect(courseTitle).toEqual(props.courseMetadata.title);
|
||||
expect(courseNumber).toEqual(props.courseMetadata.number);
|
||||
expect(courseOrg).toEqual(props.courseMetadata.org);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
42
src/__snapshots__/App.test.jsx.snap
Normal file
42
src/__snapshots__/App.test.jsx.snap
Normal file
@@ -0,0 +1,42 @@
|
||||
// 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"
|
||||
/>
|
||||
<DemoWarning />
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
|
||||
exports[`App router component snapshot: enabled 1`] = `
|
||||
<BrowserRouter>
|
||||
<div>
|
||||
<Head />
|
||||
<Header
|
||||
courseNumber="course-number"
|
||||
courseOrg="course-org"
|
||||
courseTitle="course-title"
|
||||
/>
|
||||
<CTA />
|
||||
<main>
|
||||
<ListView />
|
||||
</main>
|
||||
<Footer
|
||||
logo="https://edx-cdn.org/v3/stage/open-edx-tag.svg"
|
||||
/>
|
||||
</div>
|
||||
</BrowserRouter>
|
||||
`;
|
||||
24
src/__snapshots__/index.test.jsx.snap
Normal file
24
src/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,24 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<AppProvider
|
||||
store={
|
||||
Object {
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
wrapWithRouter={false}
|
||||
>
|
||||
<App />
|
||||
</AppProvider>
|
||||
`;
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { AlertModal, ActionRow, Button } from '@openedx/paragon';
|
||||
import { AlertModal, ActionRow, Button } from '@edx/paragon';
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
export const ConfirmModal = ({
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { ConfirmModal } from './ConfirmModal';
|
||||
|
||||
describe('ConfirmModal', () => {
|
||||
@@ -13,48 +12,10 @@ describe('ConfirmModal', () => {
|
||||
onCancel: jest.fn().mockName('this.props.onCancel'),
|
||||
onConfirm: jest.fn().mockName('this.props.onConfirm'),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
test('snapshot: closed', () => {
|
||||
expect(shallow(<ConfirmModal {...props} />)).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);
|
||||
test('snapshot: open', () => {
|
||||
expect(shallow(<ConfirmModal {...props} isOpen />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
23
src/components/DemoAlert/__snapshots__/index.test.jsx.snap
Normal file
23
src/components/DemoAlert/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,23 @@
|
||||
// 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,40 +1,39 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
ActionRow,
|
||||
AlertModal,
|
||||
Button,
|
||||
} from '@openedx/paragon';
|
||||
} from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
export const DemoAlert = ({
|
||||
intl: { formatMessage },
|
||||
isOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
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>
|
||||
}) => (
|
||||
<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 DemoAlert;
|
||||
export default injectIntl(DemoAlert);
|
||||
|
||||
@@ -1,32 +1,16 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import messages from './messages';
|
||||
import { formatMessage } from 'testUtils';
|
||||
import { DemoAlert } from '.';
|
||||
|
||||
describe('DemoAlert component', () => {
|
||||
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();
|
||||
test('snapshot', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
isOpen: true,
|
||||
onClose: jest.fn().mockName('props.onClose'),
|
||||
};
|
||||
expect(shallow(<DemoAlert {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
// 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 { screen } from '@testing-library/react';
|
||||
import filesize from 'filesize';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import filesize from 'filesize';
|
||||
import FilePopoverContent from '.';
|
||||
|
||||
jest.mock('filesize', () => (size) => `filesize(${size})`);
|
||||
@@ -14,26 +14,25 @@ describe('FilePopoverContent', () => {
|
||||
downloadURL: 'this-url-is.working',
|
||||
size: 6000,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FilePopoverContent {...props} />);
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('default', () => expect(el).toMatchSnapshot());
|
||||
test('invalid size', () => {
|
||||
el.setProps({
|
||||
size: null,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
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();
|
||||
test('content', () => {
|
||||
expect(el.text()).toContain(props.name);
|
||||
expect(el.text()).toContain(props.description);
|
||||
expect(el.text()).toContain(filesize(props.size));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Alert, Button } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import { Alert, Button } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
const messageShape = PropTypes.shape({
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import ErrorBanner from './ErrorBanner';
|
||||
|
||||
import messages from '../messages';
|
||||
|
||||
describe('Error Banner component', () => {
|
||||
@@ -23,29 +25,35 @@ describe('Error Banner component', () => {
|
||||
children,
|
||||
};
|
||||
|
||||
describe('behavior', () => {
|
||||
it('renders children content', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const childText = screen.getByText('Abitary Child');
|
||||
expect(childText).toBeInTheDocument();
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ErrorBanner {...props} />);
|
||||
});
|
||||
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('children node', () => {
|
||||
expect(el.containsMatchingElement(children)).toEqual(true);
|
||||
});
|
||||
|
||||
it('renders the correct number of action buttons', () => {
|
||||
renderWithIntl(<ErrorBanner {...props} />);
|
||||
const buttons = screen.getAllByText(messages.retryButton.defaultMessage);
|
||||
expect(buttons).toHaveLength(2);
|
||||
test('verify actions', () => {
|
||||
const actions = el.find('Alert').prop('actions');
|
||||
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 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');
|
||||
test('verify heading', () => {
|
||||
const heading = el.find('FormattedMessage');
|
||||
expect(heading.props()).toEqual(props.headingMessage);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
|
||||
import { Alert, Spinner } from '@openedx/paragon';
|
||||
import { Alert, Spinner } from '@edx/paragon';
|
||||
|
||||
export const LoadingBanner = () => (
|
||||
<Alert variant="info">
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import LoadingBanner from './LoadingBanner';
|
||||
|
||||
describe('Loading Banner component', () => {
|
||||
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');
|
||||
});
|
||||
test('snapshot', () => {
|
||||
const el = shallow(<LoadingBanner />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Error Banner component snapshot 1`] = `
|
||||
<Alert
|
||||
actions={
|
||||
Array [
|
||||
<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>
|
||||
`;
|
||||
@@ -0,0 +1,12 @@
|
||||
// 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,40 +1,21 @@
|
||||
import React from 'react';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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'),
|
||||
};
|
||||
|
||||
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');
|
||||
props.onError = jest.fn().mockName('this.props.onError');
|
||||
props.onSuccess = jest.fn().mockName('this.props.onSuccess');
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ImageRenderer {...props} />);
|
||||
});
|
||||
|
||||
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();
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,15 +1,17 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Document, Page, pdfjs } from 'react-pdf';
|
||||
|
||||
import { pdfjs, Document, Page } from 'react-pdf';
|
||||
import {
|
||||
Icon, Form, ActionRow, IconButton,
|
||||
} from '@openedx/paragon';
|
||||
import { ChevronLeft, ChevronRight } from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon';
|
||||
import { ChevronLeft, ChevronRight } from '@edx/paragon/icons';
|
||||
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
import { rendererHooks } from './pdfHooks';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = `//unpkg.com/pdfjs-dist@${pdfjs.version}/build/pdf.worker.min.js`;
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
/**
|
||||
* <PDFRenderer />
|
||||
|
||||
@@ -1,22 +1,16 @@
|
||||
import { Document, Page } from 'react-pdf';
|
||||
import { render } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import PDFRenderer from './PDFRenderer';
|
||||
|
||||
import * as hooks from './pdfHooks';
|
||||
|
||||
jest.mock('react-pdf', () => ({
|
||||
pdfjs: { GlobalWorkerOptions: {} },
|
||||
Document: jest.fn(),
|
||||
Page: jest.fn(),
|
||||
Document: () => 'Document',
|
||||
Page: () => 'Page',
|
||||
}));
|
||||
|
||||
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(),
|
||||
}));
|
||||
@@ -39,45 +33,25 @@ describe('PDF Renderer Component', () => {
|
||||
onNextPageButtonClick: jest.fn().mockName('hooks.onNextPageButtonClick'),
|
||||
onPrevPageButtonClick: jest.fn().mockName('hooks.onPrevPageButtonClick'),
|
||||
hasNext: true,
|
||||
hasPrev: false,
|
||||
hasPref: false,
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('rendering', () => {
|
||||
it('should render the PDF document with navigation controls', () => {
|
||||
describe('snapshots', () => {
|
||||
test('first page, prev is disabled', () => {
|
||||
hooks.rendererHooks.mockReturnValue(hookProps);
|
||||
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);
|
||||
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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', () => {
|
||||
test('on last page, next is disabled', () => {
|
||||
hooks.rendererHooks.mockReturnValue({
|
||||
...hookProps,
|
||||
pageNumber: hookProps.numPages,
|
||||
hasNext: false,
|
||||
hasPrev: true,
|
||||
});
|
||||
|
||||
const { container } = render(<PDFRenderer {...props} />);
|
||||
const nextButton = container.querySelector('button[aria-label="next pdf page"]');
|
||||
expect(nextButton).toBeDisabled();
|
||||
expect(shallow(<PDFRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,38 +1,23 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import TXTRenderer from './TXTRenderer';
|
||||
|
||||
jest.mock('./textHooks', () => {
|
||||
const mockRendererHooks = jest.fn().mockReturnValue({ content: 'test-content' });
|
||||
const content = 'test-content';
|
||||
return {
|
||||
rendererHooks: mockRendererHooks,
|
||||
content,
|
||||
rendererHooks: (args) => ({ content, rendererHooks: args }),
|
||||
};
|
||||
});
|
||||
|
||||
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'),
|
||||
};
|
||||
|
||||
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,
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(shallow(<TXTRenderer {...props} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
// 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"
|
||||
/>
|
||||
`;
|
||||
@@ -0,0 +1,137 @@
|
||||
// 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={
|
||||
Object {
|
||||
"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={
|
||||
Object {
|
||||
"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>
|
||||
`;
|
||||
@@ -0,0 +1,9 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`TXT Renderer Component snapshot 1`] = `
|
||||
<pre
|
||||
className="txt-renderer"
|
||||
>
|
||||
test-content
|
||||
</pre>
|
||||
`;
|
||||
@@ -1,11 +1,16 @@
|
||||
import { useState, useRef } from 'react';
|
||||
|
||||
import { pdfjs } from 'react-pdf';
|
||||
import pdfjsWorker from 'react-pdf/dist/esm/pdf.worker.entry';
|
||||
|
||||
import 'react-pdf/dist/esm/Page/AnnotationLayer.css';
|
||||
|
||||
import { ErrorStatuses } from 'data/constants/requests';
|
||||
import { StrictDict } from 'utils';
|
||||
import * as module from './pdfHooks';
|
||||
|
||||
pdfjs.GlobalWorkerOptions.workerSrc = pdfjsWorker;
|
||||
|
||||
export const errors = StrictDict({
|
||||
missingPDF: 'MissingPDFException',
|
||||
});
|
||||
|
||||
@@ -12,11 +12,6 @@ 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,11 +10,6 @@ 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,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Card, Collapsible } from '@openedx/paragon';
|
||||
import { Card, Collapsible } from '@edx/paragon';
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
|
||||
@@ -17,7 +17,7 @@ export const FileCard = ({ file, children }) => (
|
||||
defaultOpen
|
||||
title={<h3 className="file-card-title">{file.name}</h3>}
|
||||
>
|
||||
<div className="preview-panel" data-testid="preview-panel">
|
||||
<div className="preview-panel">
|
||||
<FileInfo><FilePopoverContent {...file} /></FileInfo>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
.file-card {
|
||||
margin: var(--pgn-spacing-spacer-1) 0;
|
||||
margin: map-get($spacers, 1) 0;
|
||||
|
||||
.file-card-title {
|
||||
text-overflow: ellipsis;
|
||||
@@ -24,8 +26,8 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (--pgn-size-breakpoint-max-width-sm) {
|
||||
@include media-breakpoint-down(sm) {
|
||||
.file-card-title {
|
||||
width: calc(var(--pgn-size-container-max-width-sm)/2);
|
||||
width: calc(map-get($container-max-widths, "sm")/2);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,10 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Collapsible } from '@edx/paragon';
|
||||
|
||||
import FilePopoverContent from 'components/FilePopoverContent';
|
||||
import FileInfo from './FileInfo';
|
||||
import FileCard from './FileCard';
|
||||
|
||||
jest.mock('components/FilePopoverContent', () => 'FilePopoverContent');
|
||||
@@ -13,27 +19,24 @@ describe('File Preview Card component', () => {
|
||||
},
|
||||
};
|
||||
const children = (<h1>some children</h1>);
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<FileCard {...props}>{children}</FileCard>);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
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('collapsible title is name header', () => {
|
||||
const title = el.find(Collapsible).prop('title');
|
||||
expect(title).toEqual(<h3 className="file-card-title">{props.file.name}</h3>);
|
||||
});
|
||||
|
||||
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();
|
||||
test('forwards children into preview-panel', () => {
|
||||
const previewPanelChildren = el.find('.preview-panel').children();
|
||||
expect(previewPanelChildren.at(0).equals(
|
||||
<FileInfo><FilePopoverContent file={props.file} /></FileInfo>,
|
||||
));
|
||||
expect(previewPanelChildren.at(1).equals(children)).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -5,8 +5,8 @@ import {
|
||||
Button,
|
||||
OverlayTrigger,
|
||||
Popover,
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { nullMethod } from 'hooks';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -1,29 +1,25 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Popover } from '@edx/paragon';
|
||||
|
||||
import FileInfo from './FileInfo';
|
||||
import messages from './messages';
|
||||
|
||||
describe('FileInfo component', () => {
|
||||
describe('File Preview Card component', () => {
|
||||
const children = (<h1>some Children</h1>);
|
||||
const props = { onClick: jest.fn().mockName('this.props.onClick') };
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
el = shallow(<FileInfo {...props}>{children}</FileInfo>);
|
||||
});
|
||||
|
||||
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);
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
test('overlay with passed children', () => {
|
||||
const { overlay } = el.at(0).props();
|
||||
expect(overlay.type).toEqual(Popover);
|
||||
expect(overlay.props.children).toEqual(<Popover.Content>{children}</Popover.Content>);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { injectIntl, intlShape } 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,6 +39,8 @@ FileRenderer.propTypes = {
|
||||
name: PropTypes.string,
|
||||
downloadUrl: PropTypes.string,
|
||||
}).isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default FileRenderer;
|
||||
export default injectIntl(FileRenderer);
|
||||
|
||||
@@ -1,79 +1,53 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
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', () => {
|
||||
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();
|
||||
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} />)).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} />)).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
// 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"
|
||||
>
|
||||
<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>
|
||||
`;
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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>
|
||||
`;
|
||||
@@ -0,0 +1,33 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`FileRenderer component snapshot is not loading, with error 1`] = `
|
||||
<FileCard
|
||||
file={
|
||||
Object {
|
||||
"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={
|
||||
Object {
|
||||
"downloadUrl": "file download url",
|
||||
"name": "filename.txt",
|
||||
}
|
||||
}
|
||||
key="file download url"
|
||||
>
|
||||
<Renderer
|
||||
prop="hooks.rendererProps"
|
||||
/>
|
||||
</FileCard>
|
||||
`;
|
||||
@@ -79,7 +79,7 @@ export const renderHooks = ({
|
||||
message: messages.retryButton,
|
||||
};
|
||||
const error = {
|
||||
headingMessage: errorMessage,
|
||||
headerMessage: errorMessage,
|
||||
children: intl.formatMessage(errorMessage),
|
||||
actions: [errorAction],
|
||||
};
|
||||
|
||||
@@ -55,7 +55,7 @@ describe('FilePreview hooks', () => {
|
||||
});
|
||||
describe('error', () => {
|
||||
it('loads message from current error status, if valid, else from serverError', () => {
|
||||
expect(hook.error.headingMessage).toEqual(
|
||||
expect(hook.error.headerMessage).toEqual(
|
||||
hooks.ERROR_STATUSES[ErrorStatuses.serverError],
|
||||
);
|
||||
expect(hook.error.children).toEqual(
|
||||
@@ -63,7 +63,7 @@ describe('FilePreview hooks', () => {
|
||||
);
|
||||
state.mockVal(state.keys.errorStatus, ErrorStatuses.notFound);
|
||||
hook = hooks.renderHooks({ intl: { formatMessage }, file });
|
||||
expect(hook.error.headingMessage).toEqual(
|
||||
expect(hook.error.headerMessage).toEqual(
|
||||
hooks.ERROR_STATUSES[ErrorStatuses.notFound],
|
||||
);
|
||||
expect(hook.error.children).toEqual(
|
||||
|
||||
14
src/components/Head/__snapshots__/index.test.jsx.snap
Normal file
14
src/components/Head/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,14 @@
|
||||
// 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,45 +1,25 @@
|
||||
import { render } from '@testing-library/react';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import React from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { shallow } from 'enzyme';
|
||||
import Head from '.';
|
||||
|
||||
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: 'Helmet',
|
||||
}));
|
||||
|
||||
Helmet.mockImplementation(({ children }) => <div data-testid="helmet-mock">{children}</div>);
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({
|
||||
getConfig: () => ({
|
||||
SITE_NAME: 'site-name',
|
||||
FAVICON_URL: 'favicon-url',
|
||||
}),
|
||||
}));
|
||||
|
||||
describe('Head', () => {
|
||||
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');
|
||||
});
|
||||
it('snapshot', () => {
|
||||
const el = shallow(<Head />);
|
||||
expect(el).toMatchSnapshot();
|
||||
|
||||
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');
|
||||
expect(el.find('title').text()).toContain(getConfig().SITE_NAME);
|
||||
expect(el.find('link').prop('href')).toEqual(getConfig().FAVICON_URL);
|
||||
});
|
||||
});
|
||||
|
||||
29
src/components/InfoPopover/__snapshots__/index.test.jsx.snap
Normal file
29
src/components/InfoPopover/__snapshots__/index.test.jsx.snap
Normal file
@@ -0,0 +1,29 @@
|
||||
// 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"
|
||||
iconAs="Icon"
|
||||
onClick={[MockFunction this.props.onClick]}
|
||||
src={[MockFunction icons.InfoOutline]}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
`;
|
||||
@@ -6,9 +6,9 @@ import {
|
||||
Popover,
|
||||
Icon,
|
||||
IconButton,
|
||||
} from '@openedx/paragon';
|
||||
import { InfoOutline } from '@openedx/paragon/icons';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
} from '@edx/paragon';
|
||||
import { InfoOutline } from '@edx/paragon/icons';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { nullMethod } from 'hooks';
|
||||
|
||||
@@ -17,35 +17,26 @@ import messages from './messages';
|
||||
/**
|
||||
* <InfoPopover />
|
||||
*/
|
||||
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>
|
||||
);
|
||||
};
|
||||
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"
|
||||
src={InfoOutline}
|
||||
alt={intl.formatMessage(messages.altText)}
|
||||
iconAs={Icon}
|
||||
onClick={onClick}
|
||||
/>
|
||||
</OverlayTrigger>
|
||||
);
|
||||
|
||||
InfoPopover.defaultProps = {
|
||||
onClick: nullMethod,
|
||||
@@ -56,6 +47,7 @@ InfoPopover.propTypes = {
|
||||
PropTypes.arrayOf(PropTypes.node),
|
||||
PropTypes.node,
|
||||
]).isRequired,
|
||||
intl: intlShape.isRequired,
|
||||
};
|
||||
|
||||
export default InfoPopover;
|
||||
export default injectIntl(InfoPopover);
|
||||
|
||||
@@ -1,31 +1,23 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { formatMessage } 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).toMatchSnapshot();
|
||||
});
|
||||
describe('Component', () => {
|
||||
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();
|
||||
test('Test component render', () => {
|
||||
expect(el.length).toEqual(1);
|
||||
expect(el.find('.esg-help-icon').length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Spinner } from '@openedx/paragon';
|
||||
import { Spinner } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Badge } from '@openedx/paragon';
|
||||
import { Badge } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
@@ -1,36 +1,34 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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 { container } = renderWithIntl(<StatusBadge className={className} status="arbitrary" />);
|
||||
expect(container.firstChild).toBeNull();
|
||||
const el = render('arbitrary');
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
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');
|
||||
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).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('`locked` shows light button variant and message', () => {
|
||||
const el = render(gradingStatuses.locked);
|
||||
expect(el).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('`graded` shows success button variant and message', () => {
|
||||
const el = render(gradingStatuses.graded);
|
||||
expect(el).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');
|
||||
test('`inProgress` shows warning button variant and message', () => {
|
||||
const el = render(gradingStatuses.inProgress);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
59
src/components/__snapshots__/ConfirmModal.test.jsx.snap
Normal file
59
src/components/__snapshots__/ConfirmModal.test.jsx.snap
Normal file
@@ -0,0 +1,59 @@
|
||||
// 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>
|
||||
`;
|
||||
55
src/components/__snapshots__/StatusBadge.test.jsx.snap
Normal file
55
src/components/__snapshots__/StatusBadge.test.jsx.snap
Normal file
@@ -0,0 +1,55 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`StatusBadge component behavior does not render if status does not have configured variant 1`] = `""`;
|
||||
|
||||
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>
|
||||
`;
|
||||
11
src/containers/CTA/CTA.test.jsx
Normal file
11
src/containers/CTA/CTA.test.jsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { CTA } from '.';
|
||||
|
||||
describe('CTA component', () => {
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<CTA hide />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
31
src/containers/CTA/__snapshots__/CTA.test.jsx.snap
Normal file
31
src/containers/CTA/__snapshots__/CTA.test.jsx.snap
Normal file
@@ -0,0 +1,31 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`CTA component snapshots 1`] = `
|
||||
<PageBanner>
|
||||
<span>
|
||||
<FormattedMessage
|
||||
defaultMessage="Thanks for using the new ORA staff grading experience. "
|
||||
description="Thank user for using ora and ask for feed back"
|
||||
id="ora-grading.CTA.feedbackMessage"
|
||||
/>
|
||||
<Hyperlink
|
||||
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
|
||||
isInline={true}
|
||||
showLaunchIcon={false}
|
||||
target="_blank"
|
||||
variant="muted"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="Provide some feedback"
|
||||
description="placeholder for the feedback anchor link"
|
||||
id="ora-grading.CTA.linkMessage"
|
||||
/>
|
||||
</Hyperlink>
|
||||
<FormattedMessage
|
||||
defaultMessage=" and let us know what you think!"
|
||||
description="inform user to provide feedback"
|
||||
id="ora-grading.CTA.letUsKnowMessage"
|
||||
/>
|
||||
</span>
|
||||
</PageBanner>
|
||||
`;
|
||||
29
src/containers/CTA/index.jsx
Normal file
29
src/containers/CTA/index.jsx
Normal file
@@ -0,0 +1,29 @@
|
||||
import React from 'react';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { PageBanner, Hyperlink } from '@edx/paragon';
|
||||
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
* <CTA />
|
||||
*/
|
||||
export const CTA = () => (
|
||||
<PageBanner>
|
||||
<span>
|
||||
<FormattedMessage {...messages.ctaFeedbackMessage} />
|
||||
<Hyperlink
|
||||
isInline
|
||||
variant="muted"
|
||||
destination="https://docs.google.com/forms/d/1Hu1rgJcCHl5_EtDb5Up3hiZ40sSUtkZQfRHJ3fWOvfQ/edit"
|
||||
target="_blank"
|
||||
showLaunchIcon={false}
|
||||
>
|
||||
<FormattedMessage {...messages.ctaLinkMessage} />
|
||||
</Hyperlink>
|
||||
<FormattedMessage {...messages.ctaLetUsKnowMessage} />
|
||||
</span>
|
||||
</PageBanner>
|
||||
);
|
||||
|
||||
export default CTA;
|
||||
23
src/containers/CTA/messages.js
Normal file
23
src/containers/CTA/messages.js
Normal file
@@ -0,0 +1,23 @@
|
||||
/* eslint-disable quotes */
|
||||
import { defineMessages } from '@edx/frontend-platform/i18n';
|
||||
import { StrictDict } from 'utils';
|
||||
|
||||
const messages = defineMessages({
|
||||
ctaFeedbackMessage: {
|
||||
id: 'ora-grading.CTA.feedbackMessage',
|
||||
defaultMessage: 'Thanks for using the new ORA staff grading experience. ',
|
||||
description: 'Thank user for using ora and ask for feed back',
|
||||
},
|
||||
ctaLinkMessage: {
|
||||
id: 'ora-grading.CTA.linkMessage',
|
||||
defaultMessage: 'Provide some feedback',
|
||||
description: 'placeholder for the feedback anchor link',
|
||||
},
|
||||
ctaLetUsKnowMessage: {
|
||||
id: 'ora-grading.CTA.letUsKnowMessage',
|
||||
defaultMessage: ' and let us know what you think!',
|
||||
description: 'inform user to provide feedback',
|
||||
},
|
||||
});
|
||||
|
||||
export default StrictDict(messages);
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { feedbackRequirement } from 'data/services/lms/constants';
|
||||
import { actions, selectors } from 'data/redux';
|
||||
@@ -12,56 +12,59 @@ import messages from './messages';
|
||||
/**
|
||||
* <CriterionFeedback />
|
||||
*/
|
||||
export const CriterionFeedback = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
setValue,
|
||||
value,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const onChange = (event) => {
|
||||
setValue({
|
||||
value: event.target.value,
|
||||
orderNum,
|
||||
});
|
||||
};
|
||||
|
||||
const translate = (msg) => intl.formatMessage(msg);
|
||||
|
||||
const getCommentMessage = () => {
|
||||
let commentMessage = translate(isGrading ? messages.addComments : messages.comments);
|
||||
if (config === feedbackRequirement.optional) {
|
||||
commentMessage += ` ${translate(messages.optional)}`;
|
||||
}
|
||||
return commentMessage;
|
||||
};
|
||||
|
||||
if (config === feedbackRequirement.disabled) {
|
||||
return null;
|
||||
export class CriterionFeedback extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
onChange(event) {
|
||||
this.props.setValue({
|
||||
value: event.target.value,
|
||||
orderNum: this.props.orderNum,
|
||||
});
|
||||
}
|
||||
|
||||
get commentMessage() {
|
||||
const { config, isGrading } = this.props;
|
||||
let commentMessage = this.translate(isGrading ? messages.addComments : messages.comments);
|
||||
if (config === feedbackRequirement.optional) {
|
||||
commentMessage += ` ${this.translate(messages.optional)}`;
|
||||
}
|
||||
return commentMessage;
|
||||
}
|
||||
|
||||
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"
|
||||
floatingLabel={this.commentMessage}
|
||||
value={value}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
/>
|
||||
{isInvalid && (
|
||||
<Form.Control.Feedback type="invalid" className="feedback-error-msg">
|
||||
{this.translate(messages.criterionFeedbackError)}
|
||||
</Form.Control.Feedback>
|
||||
)}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
CriterionFeedback.defaultProps = {
|
||||
value: '',
|
||||
@@ -70,6 +73,8 @@ 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,
|
||||
@@ -87,4 +92,6 @@ export const mapDispatchToProps = {
|
||||
setValue: actions.grading.setCriterionFeedback,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback);
|
||||
export default injectIntl(
|
||||
connect(mapStateToProps, mapDispatchToProps)(CriterionFeedback),
|
||||
);
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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 { renderWithIntl } from '../../testUtils';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -36,6 +36,7 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Criterion Feedback', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
config: 'config string',
|
||||
isGrading: true,
|
||||
@@ -44,50 +45,119 @@ 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.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('is graded', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
test('feedback value is invalid', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
Object.values(feedbackRequirement).forEach((requirement) => {
|
||||
test(`feedback is configured to ${requirement}`, () => {
|
||||
el.setProps({
|
||||
config: requirement,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('render', () => {
|
||||
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 grading (the feedback input is not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
expect(el.instance().props.value).toEqual(props.value);
|
||||
const controlEl = el.find('.feedback-input');
|
||||
expect(controlEl.prop('disabled')).toEqual(false);
|
||||
expect(controlEl.prop('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 graded (the input is disabled)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el.instance().props.value).toEqual(props.value);
|
||||
const controlEl = el.find('.feedback-input');
|
||||
expect(controlEl.prop('disabled')).toEqual(true);
|
||||
expect(controlEl.prop('value')).toEqual(props.value);
|
||||
});
|
||||
|
||||
it('displays an error message when feedback is invalid', () => {
|
||||
renderWithIntl(<CriterionFeedback {...props} isInvalid />);
|
||||
expect(screen.getByTestId('criterion-feedback-error-msg')).toBeInTheDocument();
|
||||
test('is having invalid feedback (feedback get render)', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
const feedbackErrorEl = el.find('.feedback-error-msg');
|
||||
expect(el.instance().props.isInvalid).toEqual(true);
|
||||
expect(feedbackErrorEl).toBeDefined();
|
||||
});
|
||||
|
||||
it('does not render anything when config is set to disabled', () => {
|
||||
const { container } = renderWithIntl(
|
||||
<CriterionFeedback {...props} config={feedbackRequirement.disabled} />,
|
||||
);
|
||||
expect(container.firstChild).toBeNull();
|
||||
test('is configure to disabled (the input does not get render)', () => {
|
||||
el.setProps({
|
||||
config: feedbackRequirement.disabled,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('behavior', () => {
|
||||
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,
|
||||
test('onChange call set value', () => {
|
||||
el = shallow(<CriterionFeedback {...props} />);
|
||||
el.instance().onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
});
|
||||
expect(props.setValue).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getter commentMessage', () => {
|
||||
test('is grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: true });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.addComments.defaultMessage,
|
||||
);
|
||||
});
|
||||
|
||||
test('is not grading', () => {
|
||||
el.setProps({ config: feedbackRequirement.optional, isGrading: false });
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
el.setProps({ config: feedbackRequirement.required });
|
||||
expect(el.instance().commentMessage).not.toContain(
|
||||
messages.optional.defaultMessage,
|
||||
);
|
||||
|
||||
expect(el.instance().commentMessage).toContain(
|
||||
messages.comments.defaultMessage,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -99,17 +169,17 @@ describe('Criterion Feedback', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
it('gets config from selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
test('selectors.app.rubric.criterionFeedbackConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionFeedbackConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
it('gets value from selectors.grading.selected.criterionFeedback', () => {
|
||||
test('selector.grading.selected.criterionFeedback', () => {
|
||||
expect(mapped.value).toEqual(
|
||||
selectors.grading.selected.criterionFeedback(testState, ownProps),
|
||||
);
|
||||
});
|
||||
it('gets isInvalid from selectors.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
test('selector.grading.validation.criterionFeedbackIsInvalid', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionFeedbackIsInvalid(
|
||||
testState,
|
||||
@@ -120,7 +190,7 @@ describe('Criterion Feedback', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
expect(mapDispatchToProps.setValue).toEqual(
|
||||
actions.grading.setCriterionFeedback,
|
||||
);
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Form } from '@edx/paragon';
|
||||
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { actions, selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
@@ -11,46 +11,50 @@ import messages from './messages';
|
||||
/**
|
||||
* <RadioCriterion />
|
||||
*/
|
||||
export const RadioCriterion = ({
|
||||
orderNum,
|
||||
isGrading,
|
||||
config,
|
||||
data,
|
||||
setCriterionOption,
|
||||
isInvalid,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
export class RadioCriterion extends React.Component {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.onChange = this.onChange.bind(this);
|
||||
}
|
||||
|
||||
const onChange = (event) => {
|
||||
setCriterionOption({
|
||||
orderNum,
|
||||
onChange(event) {
|
||||
this.props.setCriterionOption({
|
||||
orderNum: this.props.orderNum,
|
||||
value: event.target.value,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
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>
|
||||
);
|
||||
};
|
||||
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"
|
||||
key={option.name}
|
||||
value={option.name}
|
||||
description={intl.formatMessage(messages.optionPoints, { points: option.points })}
|
||||
onChange={this.onChange}
|
||||
disabled={!isGrading}
|
||||
>
|
||||
{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: {
|
||||
@@ -62,6 +66,8 @@ RadioCriterion.defaultProps = {
|
||||
RadioCriterion.propTypes = {
|
||||
orderNum: PropTypes.number.isRequired,
|
||||
isGrading: PropTypes.bool.isRequired,
|
||||
// injected
|
||||
intl: intlShape.isRequired,
|
||||
// redux
|
||||
config: PropTypes.shape({
|
||||
prompt: PropTypes.string,
|
||||
@@ -92,4 +98,4 @@ export const mapDispatchToProps = {
|
||||
setCriterionOption: actions.grading.setCriterionOption,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(RadioCriterion);
|
||||
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(RadioCriterion));
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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: {
|
||||
@@ -30,6 +31,7 @@ jest.mock('data/redux/grading/selectors', () => ({
|
||||
|
||||
describe('Radio Criterion Container', () => {
|
||||
const props = {
|
||||
intl: { formatMessage },
|
||||
orderNum: 1,
|
||||
isGrading: true,
|
||||
config: {
|
||||
@@ -53,47 +55,77 @@ describe('Radio Criterion Container', () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
data: 'option name',
|
||||
data: 'selected radio option',
|
||||
setCriterionOption: jest.fn().mockName('this.props.setCriterionOption'),
|
||||
isInvalid: false,
|
||||
};
|
||||
|
||||
describe('component rendering', () => {
|
||||
it('should render radio buttons that are enabled when in grading mode', () => {
|
||||
const { container } = renderWithIntl(<RadioCriterion {...props} />);
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance().onChange = jest.fn().mockName('this.onChange');
|
||||
});
|
||||
describe('snapshot', () => {
|
||||
test('is grading', () => {
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
const radioButtons = container.querySelectorAll('input[type="radio"]');
|
||||
expect(radioButtons.length).toEqual(props.config.options.length);
|
||||
test('is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
|
||||
radioButtons.forEach(button => {
|
||||
expect(button).not.toBeDisabled();
|
||||
test('radio contain invalid response', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.instance().render()).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
describe('rendering', () => {
|
||||
test('is grading (all options are not disabled)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(false));
|
||||
});
|
||||
|
||||
test('is not grading (all options are disabled)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl) => expect(optionEl.prop('disabled')).toEqual(true));
|
||||
});
|
||||
|
||||
test('radio contain invalid response (error response get render)', () => {
|
||||
el.setProps({
|
||||
isInvalid: true,
|
||||
});
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const radioErrorEl = el.find('.feedback-error-msg');
|
||||
expect(el.instance().props.isInvalid).toEqual(true);
|
||||
expect(radioErrorEl).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
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();
|
||||
describe('behavior', () => {
|
||||
test('onChange call set crition option', () => {
|
||||
el = shallow(<RadioCriterion {...props} />);
|
||||
el.instance().onChange({
|
||||
target: {
|
||||
value: 'some value',
|
||||
},
|
||||
});
|
||||
expect(props.setCriterionOption).toBeCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
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', () => {
|
||||
@@ -103,20 +135,18 @@ describe('Radio Criterion Container', () => {
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
|
||||
it('should properly map config from rubric criterion config selector', () => {
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly map data from selected criterion option selector', () => {
|
||||
test('selectors.grading.selected.criterionSelectedOption', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionSelectedOption(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
it('should properly map isInvalid from criterion validation selector', () => {
|
||||
test('selectors.grading.validation.criterionSelectedOptionIsInvalid', () => {
|
||||
expect(mapped.isInvalid).toEqual(
|
||||
selectors.grading.validation.criterionSelectedOptionIsInvalid(testState, ownProps),
|
||||
);
|
||||
@@ -124,7 +154,7 @@ describe('Radio Criterion Container', () => {
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('should map setCriterionOption action to props', () => {
|
||||
test('maps actions.grading.setCriterionFeedback to setValue prop', () => {
|
||||
expect(mapDispatchToProps.setCriterionOption).toEqual(
|
||||
actions.grading.setCriterionOption,
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form, FormControlFeedback } from '@openedx/paragon';
|
||||
import { Form, FormControlFeedback } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
@@ -14,10 +14,10 @@ import messages from './messages';
|
||||
export const ReviewCriterion = ({ config }) => (
|
||||
<div className="review-criterion">
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="criteria-option" data-testid="criteria-option">
|
||||
<div key={option.name} className="criteria-option">
|
||||
<div>
|
||||
<Form.Label className="option-label" data-testid="option-label">{option.label}</Form.Label>
|
||||
<FormControlFeedback className="option-points" data-testid="option-points">
|
||||
<Form.Label className="option-label">{option.label}</Form.Label>
|
||||
<FormControlFeedback className="option-points">
|
||||
<FormattedMessage {...messages.optionPoints} values={{ points: option.points }} />
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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 Criterion Container', () => {
|
||||
describe('Review Crition Container', () => {
|
||||
const props = {
|
||||
orderNum: 1,
|
||||
config: {
|
||||
@@ -50,20 +50,29 @@ describe('Review Criterion Container', () => {
|
||||
},
|
||||
};
|
||||
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<ReviewCriterion {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
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`);
|
||||
test('rendering (everything show up)', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.criteria-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
const option = props.config.options[i];
|
||||
expect(optionEl.key()).toEqual(option.name);
|
||||
expect(optionEl.find('.option-label').childAt(0).text()).toEqual(
|
||||
option.label,
|
||||
);
|
||||
expect(optionEl.find('.option-points').childAt(0).props()).toEqual({
|
||||
...messages.optionPoints,
|
||||
values: { points: option.points },
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -72,18 +81,16 @@ describe('Review Criterion Container', () => {
|
||||
const testState = { arbitrary: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
|
||||
it('should map criterion config from state', () => {
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
it('should map criterion grade data from state', () => {
|
||||
test('selectors.grading.selected.criterionGradeData', () => {
|
||||
expect(mapped.data).toEqual(
|
||||
selectors.grading.selected.criterionGradeData(testState, ownProps),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
// 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"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments (Optional)"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
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"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
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"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
<Form.Control.Feedback
|
||||
className="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"
|
||||
disabled={true}
|
||||
floatingLabel="Comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
|
||||
exports[`Criterion Feedback snapshot is grading 1`] = `
|
||||
<Form.Group>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="criterion-feedback feedback-input"
|
||||
disabled={false}
|
||||
floatingLabel="Add comments"
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="criterion value"
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
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"
|
||||
description="1 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={true}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
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"
|
||||
description="1 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
value="option name"
|
||||
>
|
||||
this label
|
||||
</Form.Radio>
|
||||
<Form.Radio
|
||||
className="criteria-option"
|
||||
description="2 points"
|
||||
disabled={false}
|
||||
onChange={[MockFunction this.onChange]}
|
||||
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>
|
||||
`;
|
||||
@@ -0,0 +1,60 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Review Crition Container snapshot 1`] = `
|
||||
<div
|
||||
className="review-criterion"
|
||||
>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
>
|
||||
this label
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
Object {
|
||||
"points": 1,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
className="criteria-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<div>
|
||||
<Form.Label
|
||||
className="option-label"
|
||||
>
|
||||
this label 2
|
||||
</Form.Label>
|
||||
<FormControlFeedback
|
||||
className="option-points"
|
||||
>
|
||||
<FormattedMessage
|
||||
defaultMessage="{points} points"
|
||||
description="criterion option point value display"
|
||||
id="ora-grading.RadioCriterion.optionPoints"
|
||||
values={
|
||||
Object {
|
||||
"points": 2,
|
||||
}
|
||||
}
|
||||
/>
|
||||
</FormControlFeedback>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="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"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="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"
|
||||
key="option name"
|
||||
>
|
||||
<strong>
|
||||
this label
|
||||
</strong>
|
||||
<br />
|
||||
explanation
|
||||
</div>
|
||||
<div
|
||||
className="help-popover-option"
|
||||
key="option name 2"
|
||||
>
|
||||
<strong>
|
||||
this label 2
|
||||
</strong>
|
||||
<br />
|
||||
explanation 2
|
||||
</div>
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div
|
||||
className="rubric-criteria"
|
||||
>
|
||||
<ReviewCriterion
|
||||
orderNum={1}
|
||||
/>
|
||||
</div>
|
||||
<CriterionFeedback
|
||||
isGrading={false}
|
||||
orderNum={1}
|
||||
/>
|
||||
</Form.Group>
|
||||
`;
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { Form } from '@openedx/paragon';
|
||||
import { Form } from '@edx/paragon';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { gradeStatuses } from 'data/services/lms/constants';
|
||||
@@ -25,7 +25,7 @@ export const CriterionContainer = (props) => {
|
||||
<span className="criteria-title">{config.prompt}</span>
|
||||
<InfoPopover>
|
||||
{config.options.map((option) => (
|
||||
<div key={option.name} className="help-popover-option" data-testid="help-popover-option">
|
||||
<div key={option.name} className="help-popover-option">
|
||||
<strong>{option.label}</strong>
|
||||
<br />
|
||||
{option.explanation}
|
||||
@@ -33,7 +33,7 @@ export const CriterionContainer = (props) => {
|
||||
))}
|
||||
</InfoPopover>
|
||||
</Form.Label>
|
||||
<div className="rubric-criteria" data-testid="rubric-criteria">
|
||||
<div className="rubric-criteria">
|
||||
{isGrading || gradeStatus === gradeStatuses.graded ? (
|
||||
<RadioCriterion orderNum={orderNum} isGrading={isGrading} />
|
||||
) : (
|
||||
|
||||
@@ -1,50 +1,15 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import PropTypes from 'prop-types';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { gradeStatuses } from 'data/services/lms/constants';
|
||||
|
||||
import { CriterionContainer, mapStateToProps } from '.';
|
||||
|
||||
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('components/InfoPopover', () => 'InfoPopover');
|
||||
jest.mock('./RadioCriterion', () => 'RadioCriterion');
|
||||
jest.mock('./CriterionFeedback', () => 'CriterionFeedback');
|
||||
jest.mock('./ReviewCriterion', () => 'ReviewCriterion');
|
||||
|
||||
jest.mock('data/redux/app/selectors', () => ({
|
||||
rubric: {
|
||||
@@ -53,18 +18,12 @@ 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,
|
||||
@@ -92,43 +51,63 @@ describe('Criterion Container', () => {
|
||||
},
|
||||
gradeStatus: gradeStatuses.ungraded,
|
||||
};
|
||||
let el;
|
||||
beforeEach(() => {
|
||||
el = shallow(<CriterionContainer {...props} />);
|
||||
});
|
||||
|
||||
describe('component rendering', () => {
|
||||
it('displays the criterion prompt', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByText('prompt')).toBeInTheDocument();
|
||||
describe('snapshot', () => {
|
||||
test('is ungraded and is grading', () => {
|
||||
expect(el).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 ungraded and is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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 graded and is not grading', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
describe('component', () => {
|
||||
test('rendering and all of the option show up', () => {
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
const optionsEl = el.find('.help-popover-option');
|
||||
expect(optionsEl.length).toEqual(props.config.options.length);
|
||||
optionsEl.forEach((optionEl, i) => {
|
||||
expect(optionEl.key()).toEqual(props.config.options[i].name);
|
||||
expect(optionEl.text()).toContain(props.config.options[i].explanation);
|
||||
});
|
||||
});
|
||||
|
||||
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 grading (Radio criterion get render)', () => {
|
||||
const rubricCriteria = el.find('.rubric-criteria');
|
||||
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
|
||||
});
|
||||
|
||||
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 ungraded and is not grading (Review criterion get render)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
});
|
||||
const rubricCriteria = el.find('.rubric-criteria');
|
||||
expect(rubricCriteria.children(0).name()).toEqual('ReviewCriterion');
|
||||
});
|
||||
|
||||
it('renders CriterionFeedback component', () => {
|
||||
render(<CriterionContainer {...props} />);
|
||||
expect(screen.getByTestId('criterion-feedback-component')).toBeInTheDocument();
|
||||
test('is graded and is not grading (Radio criterion get render)', () => {
|
||||
el.setProps({
|
||||
isGrading: false,
|
||||
gradeStatus: gradeStatuses.graded,
|
||||
});
|
||||
const rubricCriteria = el.find('.rubric-criteria');
|
||||
expect(rubricCriteria.children(0).name()).toEqual('RadioCriterion');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -136,18 +115,16 @@ describe('Criterion Container', () => {
|
||||
const testState = { arbitraryState: 'some data' };
|
||||
const ownProps = { orderNum: props.orderNum };
|
||||
let mapped;
|
||||
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState, ownProps);
|
||||
});
|
||||
|
||||
it('maps rubric criterion config to props', () => {
|
||||
test('selectors.app.rubric.criterionConfig', () => {
|
||||
expect(mapped.config).toEqual(
|
||||
selectors.app.rubric.criterionConfig(testState, ownProps),
|
||||
);
|
||||
});
|
||||
|
||||
it('maps grading status to props', () => {
|
||||
test('selectors.grading.selected.gradeStatus', () => {
|
||||
expect(mapped.gradeStatus).toEqual(
|
||||
selectors.grading.selected.gradeStatus(testState),
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import { IntlProvider } from '@edx/frontend-platform/i18n';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import { DemoWarning, mapStateToProps } from '.';
|
||||
import messages from './messages';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
@@ -10,26 +10,24 @@ jest.mock('data/redux', () => ({
|
||||
},
|
||||
}));
|
||||
|
||||
describe('DemoWarning component', () => {
|
||||
describe('behavior', () => {
|
||||
it('does not render when hide prop is true', () => {
|
||||
const { container } = render(<IntlProvider locale="en"><DemoWarning hide /></IntlProvider>);
|
||||
expect(container.firstChild).toBeNull();
|
||||
});
|
||||
let el;
|
||||
|
||||
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('DemoWarning component', () => {
|
||||
describe('snapshots', () => {
|
||||
test('does not render if disabled flag is missing', () => {
|
||||
el = shallow(<DemoWarning hide />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
test('snapshot: disabled flag is present', () => {
|
||||
el = shallow(<DemoWarning hide={false} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
it('maps hide prop from app.isEnabled selector', () => {
|
||||
const testState = { some: 'test-state' };
|
||||
const testState = { some: 'test-state' };
|
||||
test('hide is forwarded from app.isEnabled', () => {
|
||||
expect(mapStateToProps(testState).hide).toEqual(
|
||||
selectors.app.isEnabled(testState),
|
||||
);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DemoWarning component snapshots does not render if disabled flag is missing 1`] = `""`;
|
||||
|
||||
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>
|
||||
`;
|
||||
@@ -3,8 +3,8 @@ import { connect } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Alert } from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
import { Alert } from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
import messages from './messages';
|
||||
|
||||
@@ -2,7 +2,7 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
import { Hyperlink, Button } from '@openedx/paragon';
|
||||
import { Hyperlink, Button } from '@edx/paragon';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
import emptyStateSVG from './assets/empty-state.svg';
|
||||
|
||||
@@ -1,38 +1,33 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
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/empty-state.svg', () => './assets/empty-state.svg');
|
||||
jest.mock('./assets/emptyState.svg', () => './assets/emptyState.svg');
|
||||
|
||||
let el;
|
||||
|
||||
describe('EmptySubmission component', () => {
|
||||
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();
|
||||
describe('component', () => {
|
||||
const props = { courseId: 'test-course-id' };
|
||||
beforeEach(() => {
|
||||
el = shallow(<EmptySubmission {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('openResponse destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(0).props().destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button, DataTableContext } from '@openedx/paragon';
|
||||
import { Button, DataTableContext } from '@edx/paragon';
|
||||
|
||||
import * as module from './FilterStatusComponent';
|
||||
|
||||
|
||||
@@ -1,18 +1,54 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { render } from '@testing-library/react';
|
||||
import { DataTableContext } from '@openedx/paragon';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
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',
|
||||
@@ -22,98 +58,34 @@ describe('FilterStatusComponent component', () => {
|
||||
buttonClassName: 'css-class-name-for-button',
|
||||
showFilteredFields: true,
|
||||
};
|
||||
const { FilterStatusComponent } = module;
|
||||
|
||||
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>,
|
||||
);
|
||||
const hookProps = {
|
||||
clearFilters: jest.fn().mockName('hookProps.clearFilters'),
|
||||
filterNames: ['filter-name-0', 'filter-name-1'],
|
||||
};
|
||||
|
||||
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();
|
||||
const { FilterStatusComponent } = module;
|
||||
const mockHooks = (value) => {
|
||||
jest.spyOn(module, 'filterHooks').mockReturnValueOnce(value);
|
||||
};
|
||||
describe('snapshot', () => {
|
||||
describe('with filters', () => {
|
||||
test('showFilteredFields', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
test('showFilteredFields=false - hide filterTexts', () => {
|
||||
mockHooks(hookProps);
|
||||
const el = shallow(
|
||||
<FilterStatusComponent {...props} showFilteredFields={false} />,
|
||||
);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
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([]);
|
||||
test('without filters', () => {
|
||||
mockHooks({});
|
||||
const el = shallow(<FilterStatusComponent {...props} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
expect(el.isEmptyRender()).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -6,8 +6,8 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
Hyperlink,
|
||||
} from '@openedx/paragon';
|
||||
import { Info } from '@openedx/paragon/icons';
|
||||
} from '@edx/paragon';
|
||||
import { Info } from '@edx/paragon/icons';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import urls from 'data/services/lms/urls';
|
||||
|
||||
@@ -1,14 +1,19 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import userEvent from '@testing-library/user-event';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { selectors, thunkActions } from 'data/redux';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import { ListError, mapDispatchToProps, mapStateToProps } from './ListError';
|
||||
import messages from './messages';
|
||||
|
||||
import { formatMessage } from 'testUtils';
|
||||
import {
|
||||
ListError,
|
||||
mapDispatchToProps,
|
||||
mapStateToProps,
|
||||
} from './ListError';
|
||||
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: jest.fn((state) => state.courseId || 'test-course-id'),
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
},
|
||||
},
|
||||
thunkActions: {
|
||||
@@ -22,60 +27,41 @@ jest.mock('data/services/lms/urls', () => ({
|
||||
openResponse: (courseId) => `api/openResponse/${courseId}`,
|
||||
}));
|
||||
|
||||
let el;
|
||||
jest.useFakeTimers('modern');
|
||||
|
||||
describe('ListError component', () => {
|
||||
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('component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
};
|
||||
beforeEach(() => {
|
||||
props.loadSelectionForReview = jest.fn();
|
||||
props.intl = { formatMessage };
|
||||
props.initializeApp = jest.fn();
|
||||
});
|
||||
|
||||
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('render tests', () => {
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListError {...props} />);
|
||||
});
|
||||
test('snapshot', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapDispatchToProps', () => {
|
||||
it('maps initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(
|
||||
thunkActions.app.initialize,
|
||||
);
|
||||
it('loads initializeApp from thunkActions.app.initialize', () => {
|
||||
expect(mapDispatchToProps.initializeApp).toEqual(thunkActions.app.initialize);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
@import "@edx/paragon/scss/core/core";
|
||||
|
||||
span.pgn__icon.breadcrumb-arrow {
|
||||
width: 16px !important;
|
||||
height: 16px !important;
|
||||
};
|
||||
|
||||
.empty-submission {
|
||||
width: var(--pgn-size-container-max-width-sm);
|
||||
width: map-get($container-max-widths, "sm");
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
@@ -13,7 +15,7 @@ span.pgn__icon.breadcrumb-arrow {
|
||||
margin: auto;
|
||||
|
||||
> img {
|
||||
padding: var(--pgn-spacing-spacer-5);
|
||||
padding: map-get($spacers, 5);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,14 +25,4 @@ 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
|
||||
import { ArrowBack, Launch } from '@openedx/paragon/icons';
|
||||
import { Hyperlink, Icon } from '@openedx/paragon';
|
||||
import { ArrowBack, Launch } from '@edx/paragon/icons';
|
||||
import { Hyperlink, Icon } from '@edx/paragon';
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
import { selectors } from 'data/redux';
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { Hyperlink } from '@edx/paragon';
|
||||
|
||||
import * as constants from 'data/constants/app';
|
||||
import urls from 'data/services/lms/urls';
|
||||
import { selectors } from 'data/redux';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
|
||||
import {
|
||||
ListViewBreadcrumb,
|
||||
mapStateToProps,
|
||||
@@ -9,9 +15,9 @@ import {
|
||||
jest.mock('data/redux', () => ({
|
||||
selectors: {
|
||||
app: {
|
||||
courseId: jest.fn((state) => state.courseId || 'test-course-id'),
|
||||
courseId: (...args) => ({ courseId: args }),
|
||||
ora: {
|
||||
name: jest.fn((state) => state.oraName || 'test-ora-name'),
|
||||
name: (...args) => ({ oraName: args }),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -22,60 +28,41 @@ jest.mock('data/services/lms/urls', () => ({
|
||||
ora: (courseId, locationId) => `oraUrl(${courseId}, ${locationId})`,
|
||||
}));
|
||||
|
||||
jest.mock('data/constants/app', () => ({
|
||||
locationId: () => 'test-location-id',
|
||||
}));
|
||||
let el;
|
||||
|
||||
describe('ListViewBreadcrumb component', () => {
|
||||
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();
|
||||
describe('component', () => {
|
||||
const props = {
|
||||
courseId: 'test-course-id',
|
||||
oraName: 'fake-ora-name',
|
||||
};
|
||||
beforeEach(() => {
|
||||
el = shallow(<ListViewBreadcrumb {...props} />);
|
||||
});
|
||||
|
||||
it('displays ORA name in heading', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
const heading = screen.getByText(props.oraName);
|
||||
expect(heading).toBeInTheDocument();
|
||||
expect(heading).toHaveClass('h3');
|
||||
test('snapshot: empty (no list data)', () => {
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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('openResponse destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(0).props().destination,
|
||||
).toEqual(urls.openResponse(props.courseId));
|
||||
});
|
||||
|
||||
it('displays back to responses text', () => {
|
||||
renderWithIntl(<ListViewBreadcrumb {...props} />);
|
||||
expect(screen.getByText('Back to all open responses')).toBeInTheDocument();
|
||||
test('ora destination', () => {
|
||||
expect(
|
||||
el.find(Hyperlink).at(1).props().destination,
|
||||
).toEqual(urls.ora(props.courseId, constants.locationId()));
|
||||
});
|
||||
});
|
||||
|
||||
describe('mapStateToProps', () => {
|
||||
let mapped;
|
||||
const testState = { some: 'test-state' };
|
||||
|
||||
it('maps courseId from app.courseId selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
beforeEach(() => {
|
||||
mapped = mapStateToProps(testState);
|
||||
});
|
||||
test('courseId loads from app.courseId', () => {
|
||||
expect(mapped.courseId).toEqual(selectors.app.courseId(testState));
|
||||
});
|
||||
|
||||
it('maps oraName from app.ora.name selector', () => {
|
||||
const mapped = mapStateToProps(testState);
|
||||
test('oraName loads from app.ora.name', () => {
|
||||
expect(mapped.oraName).toEqual(selectors.app.ora.name(testState));
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Button } from '@openedx/paragon';
|
||||
import { Button } from '@edx/paragon';
|
||||
|
||||
import { FormattedMessage } from '@edx/frontend-platform/i18n';
|
||||
|
||||
|
||||
@@ -1,33 +1,20 @@
|
||||
import { screen } from '@testing-library/react';
|
||||
import { renderWithIntl } from '../../testUtils';
|
||||
import React from 'react';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
import { SelectedBulkAction } from './SelectedBulkAction';
|
||||
|
||||
describe('SelectedBulkAction component', () => {
|
||||
const props = {
|
||||
selectedFlatRows: [{ id: 1 }, { id: 2 }],
|
||||
handleClick: jest.fn(() => () => {}),
|
||||
handleClick: jest.fn(),
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
test('snapshots', () => {
|
||||
const el = shallow(<SelectedBulkAction {...props} handleClick={() => jest.fn()} />);
|
||||
expect(el).toMatchSnapshot();
|
||||
});
|
||||
|
||||
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} />);
|
||||
test('handleClick', () => {
|
||||
shallow(<SelectedBulkAction {...props} />);
|
||||
expect(props.handleClick).toHaveBeenCalledWith(props.selectedFlatRows);
|
||||
});
|
||||
});
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user