Compare commits

..

1 Commits

Author SHA1 Message Date
Alie Langston
8a5687bfe3 feat: gate learning assistant if learner is in exam 2023-11-13 14:36:49 -06:00
343 changed files with 18022 additions and 14012 deletions

1
.env
View File

@@ -4,7 +4,6 @@
NODE_ENV='production' NODE_ENV='production'
ACCESS_TOKEN_COOKIE_NAME='' ACCESS_TOKEN_COOKIE_NAME=''
APP_ID='learning'
BASE_URL='' BASE_URL=''
CONTACT_URL='' CONTACT_URL=''
CREDENTIALS_BASE_URL='' CREDENTIALS_BASE_URL=''

View File

@@ -4,7 +4,6 @@
NODE_ENV='development' NODE_ENV='development'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000' BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact' CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150' CREDENTIALS_BASE_URL='http://localhost:18150'

View File

@@ -4,7 +4,6 @@
NODE_ENV='test' NODE_ENV='test'
ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload'
APP_ID='learning'
BASE_URL='http://localhost:2000' BASE_URL='http://localhost:2000'
CONTACT_URL='http://localhost:18000/contact' CONTACT_URL='http://localhost:18000/contact'
CREDENTIALS_BASE_URL='http://localhost:18150' CREDENTIALS_BASE_URL='http://localhost:18150'

View File

@@ -3,5 +3,3 @@ dist/
packages/ packages/
node_modules/ node_modules/
jest.config.js jest.config.js
env.config.jsx
example.env.config.jsx

View File

@@ -1,5 +1,5 @@
// eslint-disable-next-line import/no-extraneous-dependencies // eslint-disable-next-line import/no-extraneous-dependencies
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
const config = createConfig('eslint', { const config = createConfig('eslint', {
rules: { rules: {
@@ -12,13 +12,6 @@ const config = createConfig('eslint', {
'react/no-unknown-property': 'off', 'react/no-unknown-property': 'off',
'func-names': 'off', 'func-names': 'off',
}, },
settings: {
'import/resolver': {
webpack: {
config: 'webpack.prod.config.js',
},
},
},
}); });
module.exports = config; module.exports = config;

View File

@@ -1,7 +0,0 @@
version: 2
updates:
# Adding new check for github-actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -9,35 +9,15 @@ on:
jobs: jobs:
tests: tests:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy:
matrix:
node: [18, 20]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node }}
- run: make validate.ci
- name: Archive code coverage results
uses: actions/upload-artifact@v4
with:
name: code-coverage-report-${{ matrix.node }}
# When we're only using Node 20, replace the line above with the following:
# name: code-coverage-report
path: coverage/*.*
coverage:
runs-on: ubuntu-latest
needs: tests
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v3
- name: Download code coverage results - name: Setup Nodejs Env
uses: actions/download-artifact@v4 run: echo "NODE_VER=`cat .nvmrc`" >> $GITHUB_ENV
- uses: actions/setup-node@v3
with: with:
name: code-coverage-report-20 node-version: ${{ env.NODE_VER }}
# When we're only using Node 20, replace the line above with the following: - run: make validate.ci
# name: code-coverage-report
- name: Upload coverage - name: Upload coverage
uses: codecov/codecov-action@v4 uses: codecov/codecov-action@v3
with: with:
fail_ci_if_error: true fail_ci_if_error: true
token: ${{ secrets.CODECOV_TOKEN }}

5
.gitignore vendored
View File

@@ -6,7 +6,6 @@
node_modules node_modules
npm-debug.log npm-debug.log
coverage coverage
env.config.*
dist/ dist/
src/i18n/transifex_input.json src/i18n/transifex_input.json
@@ -26,7 +25,3 @@ module.config.js
# Local environment overrides # Local environment overrides
.env.private .env.private
src/i18n/messages/
env.config.jsx

2
.nvmrc
View File

@@ -1 +1 @@
20 18

9
.tx/config Normal file
View File

@@ -0,0 +1,9 @@
[main]
host = https://www.transifex.com
[o:open-edx:p:edx-platform:r:frontend-app-learning]
file_filter = src/i18n/messages/<lang>.json
source_file = src/i18n/transifex_input.json
source_lang = en
type = KEYVALUEJSON

View File

@@ -1,17 +1,20 @@
export TRANSIFEX_RESOURCE=frontend-app-learning
transifex_langs = "ar,fr,es_419,zh_CN,pt,it,de,uk,ru,hi,fa_IR,fr_CA,it_IT,pt_PT,de_DE"
intl_imports = ./node_modules/.bin/intl-imports.js intl_imports = ./node_modules/.bin/intl-imports.js
transifex_utils = ./node_modules/.bin/transifex-utils.js transifex_utils = ./node_modules/.bin/transifex-utils.js
i18n = ./src/i18n i18n = ./src/i18n
transifex_input = $(i18n)/transifex_input.json transifex_input = $(i18n)/transifex_input.json
# This directory must match .babelrc . # This directory must match .babelrc .
transifex_temp = ./temp/babel-plugin-formatjs transifex_temp = ./temp/babel-plugin-react-intl
precommit: precommit:
npm run lint npm run lint
npm audit npm audit
requirements: requirements:
npm ci npm install
i18n.extract: i18n.extract:
# Pulling display strings from .jsx files into .json files... # Pulling display strings from .jsx files into .json files...
@@ -29,21 +32,35 @@ detect_changed_source_translations:
# Checking for changed translations... # Checking for changed translations...
git diff --exit-code $(i18n) 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 -f --mode reviewed --languages=$(transifex_langs)
else
# Experimental: OEP-58 Pulls translations using atlas
pull_translations: pull_translations:
rm -rf src/i18n/messages rm -rf src/i18n/messages
mkdir src/i18n/messages mkdir src/i18n/messages
cd src/i18n/messages \ cd src/i18n/messages \
&& atlas pull $(ATLAS_OPTIONS) \ && atlas pull --filter=$(transifex_langs) \
translations/frontend-platform/src/i18n/messages:frontend-platform \ translations/paragon/src/i18n/messages:paragon \
translations/paragon/src/i18n/messages:paragon \ translations/frontend-component-header/src/i18n/messages:frontend-component-header \
translations/frontend-component-header/src/i18n/messages:frontend-component-header \ translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \ translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
$(intl_imports) paragon frontend-component-header frontend-component-footer frontend-app-learning
endif
# This target is used by Travis. # This target is used by Travis.
validate-no-uncommitted-package-lock-changes: validate-no-uncommitted-package-lock-changes:
@@ -55,10 +72,8 @@ validate:
make validate-no-uncommitted-package-lock-changes make validate-no-uncommitted-package-lock-changes
npm run i18n_extract npm run i18n_extract
npm run lint -- --max-warnings 0 npm run lint -- --max-warnings 0
npm run types
npm run test npm run test
npm run build npm run build
npm run bundlewatch
.PHONY: validate.ci .PHONY: validate.ci
validate.ci: validate.ci:

View File

@@ -1,110 +1,77 @@
frontend-app-learning
##################### #####################
frontend-app-learning
######################
|codecov| |license| |codecov| |license|
********
Purpose Purpose
******* ********
This is the Learning MFE (micro-frontend application), which renders all This is the Learning MFE (micro-frontend application), which renders all
learner-facing course pages (like the course outline, the progress page, learner-facing course pages (like the course outline, the progress page,
actual course content, etc). actual course content, etc).
Please tag **@edx/engage-squad** on any PRs or issues. Thanks.
.. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3 .. |codecov| image:: https://codecov.io/gh/edx/frontend-app-learning/branch/master/graph/badge.svg?token=3z7XvuzTq3
:target: https://codecov.io/gh/edx/frontend-app-learning :target: https://codecov.io/gh/edx/frontend-app-learning
.. |license| image:: https://img.shields.io/badge/license-AGPL-informational .. |license| image:: https://img.shields.io/badge/license-AGPL-informational
:target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE :target: https://github.com/openedx/frontend-app-account/blob/master/LICENSE
***************
Getting Started Getting Started
*************** ***************
Prerequisites Prerequisites
============= =============
`Tutor`_ is currently recommended as a development environment for the Learning The `devstack`_ is currently recommended as a development environment for your
MFE. Most likely, it already has this MFE configured; however, you'll need to new MFE. If you start it with ``make dev.up.lms`` that should give you
make some changes in order to run it in development mode. You can refer everything you need as a companion to this frontend.
to the `relevant tutor-mfe documentation`_ for details, or follow the quick
guide below. 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 .. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development .. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe#mfe-development
To use this application, `devstack <https://github.com/openedx/devstack>`__ must be running and you must be logged into it.
Cloning and Setup - Visit http://localhost:2000/course/course-v1:edX+DemoX+Demo_Course to view the demo course. You can replace ``course-v1:edX+DemoX+Demo_Course`` with a different course key.
=================
1. Clone your new repo: Cloning and Startup
===================
.. code-block:: bash .. code-block::
git clone https://github.com/openedx/frontend-app-learning.git 1. Clone your new repo:
2. Use node v20.x. ``git clone https://github.com/openedx/frontend-app-learning.git``
The current version of the micro-frontend build scripts supports node 18. 2. Use node v18.x.
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>`_.
3. Stop the Tutor devstack, if it's running: ``tutor dev stop`` 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>`_.
4. Next, we need to tell Tutor that we're going to be running this repo in 3. Install npm dependencies:
development mode, and it should be excluded from the ``mfe`` container that
otherwise runs every MFE. Run this:
.. code-block:: bash ``cd frontend-app-learning && npm ci``
tutor mounts add /path/to/frontend-app-learning 4. Start the dev server:
5. Start Tutor in development mode. This command will start the LMS and Studio, ``npm start``
and other required MFEs like ``authn`` and ``account``, but will not start
the learning MFE, which we're going to run on the host instead of in a
container managed by Tutor. Run:
.. code-block:: bash
tutor dev start lms cms mfe
Startup
=======
1. Install npm dependencies:
.. code-block:: bash
cd frontend-app-learning && npm ci
2. Start the dev server:
.. code-block:: bash
npm run dev
Then you can access the app at http://local.openedx.io:2000/learning/
Troubleshooting
---------------
If you see an "Invalid Host header" error, then you're probably using a different domain name for your devstack such as
``local.edly.io`` or ``local.overhang.io`` (not the new recommended default, ``local.openedx.io``). In that case, run
these commands to update your devstack's domain names:
.. code-block:: bash
tutor dev stop
tutor config save --set LMS_HOST=local.openedx.io --set CMS_HOST=studio.local.openedx.io
tutor dev launch -I --skip-build
tutor dev stop learning # We will run this MFE on the host
Local module development Local module development
========================= =========================
To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js`` To develop locally on modules that are installed into this app, you'll need to create a ``module.config.js``
file (which is git-ignored) that defines where to find your local modules, for instance: file (which is git-ignored) that defines where to find your local modules, for instance::
.. code-block:: js
module.exports = { module.exports = {
/* /*
@@ -117,10 +84,10 @@ file (which is git-ignored) that defines where to find your local modules, for i
may want to use "src" if the module installs React as a peer/dev dependency. may want to use "src" if the module installs React as a peer/dev dependency.
*/ */
localModules: [ localModules: [
{ moduleName: '@openedx/paragon/scss', dir: '../paragon', dist: 'scss' }, { moduleName: '@edx/paragon/scss', dir: '../paragon', dist: 'scss' },
{ moduleName: '@openedx/paragon', dir: '../paragon', dist: 'dist' }, { moduleName: '@edx/paragon', dir: '../paragon', dist: 'dist' },
{ moduleName: '@openedx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' }, { moduleName: '@edx/frontend-enterprise', dir: '../frontend-enterprise', dist: 'src' },
{ moduleName: '@openedx/frontend-platform', dir: '../frontend-platform', dist: 'dist' }, { moduleName: '@edx/frontend-platform', dir: '../frontend-platform', dist: 'dist' },
], ],
}; };
@@ -133,14 +100,8 @@ The Learning MFE is similar to all the other Open edX MFEs. Read the Open
edX Developer Guide's section on edX Developer Guide's section on
`MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_. `MFE applications <https://edx.readthedocs.io/projects/edx-developer-docs/en/latest/developers_guide/micro_frontends_in_open_edx.html>`_.
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>`_.
Environment Variables Environment Variables
===================== ======================
This MFE is configured via environment variables supplied at build time. This MFE is configured via environment variables supplied at build time.
All micro-frontends have a shared set of required environment variables, All micro-frontends have a shared set of required environment variables,
@@ -166,7 +127,7 @@ SOCIAL_UTM_MILESTONE_CAMPAIGN
SUPPORT_URL_CALCULATOR_MATH SUPPORT_URL_CALCULATOR_MATH
A link that explains how to use the in-course calculator. You can use the A link that explains how to use the in-course calculator. You can use the
one in the example below if you don't want to have your own branded version. one in the example below, if you don't want to have your own branded version.
Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator Example: https://support.edx.org/hc/en-us/articles/360000038428-Entering-math-expressions-in-assignments-or-the-calculator
@@ -179,7 +140,7 @@ SUPPORT_URL_ID_VERIFICATION
SUPPORT_URL_VERIFIED_CERTIFICATE SUPPORT_URL_VERIFIED_CERTIFICATE
A link that explains what a verified certificate is. You can use the A link that explains what a verified certificate is. You can use the
one in the example below if you don't want to have your own branded version. one in the example below, if you don't want to have your own branded version.
Optional. Optional.
Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate Example: https://support.edx.org/hc/en-us/articles/206502008-What-is-a-verified-certificate
@@ -195,13 +156,13 @@ TWITTER_URL
A link to your Twitter account. The Twitter social-share link won't appear A link to your Twitter account. The Twitter social-share link won't appear
unless this is set. Optional. unless this is set. Optional.
Example: https://twitter.com/openedx Example: https://twitter.com/edXOnline
Getting Help Getting Help
============ ===========
If you're having trouble, we have `discussion forums`_ If you're having trouble, we have discussion forums at
where you can connect with others in the community. https://discuss.openedx.org where you can connect with others in the community.
Our real-time conversations are on Slack. You can request a `Slack Our real-time conversations are on Slack. You can request a `Slack
invitation`_, then join our `community Slack workspace`_. Because this is a invitation`_, then join our `community Slack workspace`_. Because this is a
@@ -219,18 +180,17 @@ For more information about these options, see the `Getting Help`_ page.
.. _community Slack workspace: https://openedx.slack.com/ .. _community Slack workspace: https://openedx.slack.com/
.. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6 .. _#wg-frontend channel: https://openedx.slack.com/archives/C04BM6YC7A6
.. _Getting Help: https://openedx.org/community/connect .. _Getting Help: https://openedx.org/community/connect
.. _discussion forums: https://discuss.openedx.org
Contributing Contributing
============ ============
Contributions are very welcome. Please read `How To Contribute`_ for details. Contributions are very welcome. Please read `How To Contribute`_ for details.
.. _How To Contribute: https://openedx.org/r/how-to-contribute .. _How To Contribute: https://openedx.org/r/how-to-contribute
This project is currently accepting all types of contributions, bug fixes, This project is currently accepting all types of contributions, bug fixes,
security fixes, maintenance work, or new features. However, please make sure security fixes, maintenance work, or new features. However, please make sure
to discuss your new feature idea with the maintainers before to have a discussion about your new feature idea with the maintainers prior to
beginning development to maximize the chances of your change being accepted. beginning development to maximize the chances of your change being accepted.
You can start a conversation by creating a new issue on this repo summarizing You can start a conversation by creating a new issue on this repo summarizing
your idea. your idea.

View File

@@ -1,18 +0,0 @@
# This file records information about this repo. Its use is described in OEP-55:
# https://open-edx-proposals.readthedocs.io/en/latest/processes/oep-0055-proc-project-maintainers.html
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: 'frontend-app-learning'
description: "This is the Learning MFE, which renders all learner-facing course pages."
links:
- url: "https://github.com/openedx/frontend-app-learning"
title: "Learning MFE"
icon: "Web"
annotations:
openedx.org/arch-interest-groups: ""
spec:
owner: group:committers-frontend-app-learning
type: 'website'
lifecycle: 'production'

View File

@@ -1,24 +0,0 @@
import UnitTranslationPlugin from '@edx/unit-translation-selector-plugin';
import { PLUGIN_OPERATIONS, DIRECT_PLUGIN } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
unit_title_plugin: {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'unit_title_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: UnitTranslationPlugin,
},
},
],
},
},
};
export default config;

View File

@@ -1,4 +0,0 @@
// Force all tests to run in UTC to prevent tests from being sensitive to host timezone.
module.exports = async () => {
process.env.TZ = 'UTC';
};

View File

@@ -1,6 +1,6 @@
const { createConfig } = require('@openedx/frontend-build'); const { createConfig } = require('@edx/frontend-build');
const config = createConfig('jest', { module.exports = createConfig('jest', {
setupFilesAfterEnv: [ setupFilesAfterEnv: [
'<rootDir>/src/setupTest.js', '<rootDir>/src/setupTest.js',
], ],
@@ -9,35 +9,12 @@ const config = createConfig('jest', {
'src/i18n', 'src/i18n',
'src/.*\\.exp\\..*', 'src/.*\\.exp\\..*',
], ],
// see https://github.com/axios/axios/issues/5026
moduleNameMapper: { moduleNameMapper: {
"^axios$": "axios/dist/axios.js",
// See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown // See https://stackoverflow.com/questions/72382316/jest-encountered-an-unexpected-token-react-markdown
'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js', 'react-markdown': '<rootDir>/node_modules/react-markdown/react-markdown.min.js',
'@src/(.*)': '<rootDir>/src/$1',
// Explicit mapping to ensure Jest resolves the module correctly
'@edx/frontend-lib-special-exams': '<rootDir>/node_modules/@edx/frontend-lib-special-exams',
}, },
testTimeout: 30000, testTimeout: 30000,
globalSetup: "./global-setup.js", testEnvironment: 'jsdom'
verbose: true,
testEnvironment: 'jsdom',
}); });
// delete config.testURL;
config.reporters = [...(config.reporters || []), ["jest-console-group-reporter", {
// change this setting if need to see less details for each test
// reportType: "summary" | "details",
// enable: true | false,
afterEachTest: {
enable: true,
filePaths: false,
reportType: "details",
},
afterAllTests: {
reportType: "summary",
enable: true,
filePaths: true,
},
}]];
module.exports = config;

14507
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,17 +11,13 @@
], ],
"scripts": { "scripts": {
"build": "fedx-scripts webpack", "build": "fedx-scripts webpack",
"bundlewatch": "bundlewatch", "i18n_extract": "BABEL_ENV=i18n fedx-scripts babel src --quiet > /dev/null",
"i18n_extract": "fedx-scripts formatjs extract", "lint": "fedx-scripts eslint --ext .js --ext .jsx .",
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .", "lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx .",
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
"prepare": "husky install", "prepare": "husky install",
"postinstall": "patch-package",
"snapshot": "fedx-scripts jest --updateSnapshot", "snapshot": "fedx-scripts jest --updateSnapshot",
"start": "fedx-scripts webpack-dev-server --progress", "start": "fedx-scripts webpack-dev-server --progress",
"dev": "PUBLIC_PATH=/learning/ MFE_CONFIG_API_URL='http://localhost:8000/api/mfe_config/v1' fedx-scripts webpack-dev-server --progress --host apps.local.openedx.io", "test": "fedx-scripts jest --coverage --passWithNoTests"
"test": "fedx-scripts jest --coverage --passWithNoTests",
"types": "tsc --noEmit"
}, },
"author": "edX", "author": "edX",
"license": "AGPL-3.0", "license": "AGPL-3.0",
@@ -34,33 +30,26 @@
}, },
"dependencies": { "dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2", "@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"@edx/browserslist-config": "1.2.0", "@edx/frontend-component-footer": "12.2.1",
"@edx/frontend-component-header": "^5.8.0", "@edx/frontend-component-header": "4.6.0",
"@edx/frontend-lib-learning-assistant": "^2.2.4", "@edx/frontend-lib-learning-assistant": "^1.17.0",
"@edx/frontend-lib-special-exams": "^3.1.3", "@edx/frontend-lib-special-exams": "2.23.3",
"@edx/frontend-platform": "^8.0.0", "@edx/frontend-platform": "5.5.2",
"@edx/openedx-atlas": "^0.6.0", "@edx/paragon": "20.46.0",
"@edx/react-unit-test-utils": "3.0.0", "@edx/react-unit-test-utils": "npm:@edx/react-unit-test-utils@1.7.0",
"@fortawesome/fontawesome-svg-core": "1.3.0",
"@fortawesome/free-brands-svg-icons": "5.15.4", "@fortawesome/free-brands-svg-icons": "5.15.4",
"@fortawesome/free-regular-svg-icons": "5.15.4", "@fortawesome/free-regular-svg-icons": "5.15.4",
"@fortawesome/free-solid-svg-icons": "5.15.4", "@fortawesome/free-solid-svg-icons": "5.15.4",
"@fortawesome/react-fontawesome": "^0.1.4", "@fortawesome/react-fontawesome": "^0.1.4",
"@openedx/frontend-build": "14.1.2",
"@openedx/frontend-plugin-framework": "^1.2.1",
"@openedx/frontend-slot-footer": "^1.0.2",
"@openedx/paragon": "^22.3.0",
"@popperjs/core": "2.11.8", "@popperjs/core": "2.11.8",
"@reduxjs/toolkit": "1.8.1", "@reduxjs/toolkit": "1.8.1",
"buffer": "^6.0.3", "classnames": "2.3.2",
"classnames": "2.5.1", "core-js": "3.22.2",
"copy-webpack-plugin": "^11.0.0", "history": "5.3.0",
"husky": "7.0.4",
"joi": "^17.11.0", "joi": "^17.11.0",
"js-cookie": "3.0.5", "js-cookie": "3.0.5",
"lodash": "^4.17.21",
"lodash.camelcase": "4.3.0", "lodash.camelcase": "4.3.0",
"patch-package": "^8.0.0",
"postcss-loader": "^8.1.1",
"prop-types": "15.8.1", "prop-types": "15.8.1",
"query-string": "^7.1.3", "query-string": "^7.1.3",
"react": "17.0.2", "react": "17.0.2",
@@ -71,34 +60,25 @@
"react-router-dom": "6.15.0", "react-router-dom": "6.15.0",
"react-share": "4.4.1", "react-share": "4.4.1",
"redux": "4.1.2", "redux": "4.1.2",
"regenerator-runtime": "0.13.11",
"reselect": "4.1.8", "reselect": "4.1.8",
"sass": "^1.79.3", "truncate-html": "1.0.4",
"sass-loader": "^16.0.2", "util": "0.12.5"
"source-map-loader": "^5.0.0",
"truncate-html": "1.0.4"
}, },
"devDependencies": { "devDependencies": {
"@edx/browserslist-config": "1.2.0",
"@edx/frontend-build": "^12.9.10",
"@edx/reactifex": "2.2.0", "@edx/reactifex": "2.2.0",
"@pact-foundation/pact": "^13.0.0", "@pact-foundation/pact": "^11.0.2",
"@testing-library/jest-dom": "5.17.0", "@testing-library/jest-dom": "5.16.5",
"@testing-library/react": "12.1.5", "@testing-library/react": "12.1.5",
"@testing-library/react-hooks": "^8.0.1", "@testing-library/react-hooks": "^8.0.1",
"@testing-library/user-event": "13.5.0", "@testing-library/user-event": "13.5.0",
"axios-mock-adapter": "2.0.0", "axios-mock-adapter": "1.20.0",
"bundlewatch": "^0.4.0", "copy-webpack-plugin": "^11.0.0",
"eslint-import-resolver-webpack": "^0.13.9", "es-check": "6.2.1",
"jest": "^29.7.0", "husky": "7.0.4",
"jest-console-group-reporter": "^1.1.1", "jest": "29.5.0",
"jest-when": "^3.6.0", "rosie": "2.1.0"
"rosie": "2.1.1"
},
"bundlewatch": {
"files": [
{
"path": "dist/*.js",
"maxSize": "1300kB"
}
],
"normalizeFilenames": "^.+?(\\..+?)\\.\\w+$"
} }
} }

View File

@@ -1,36 +0,0 @@
diff --git a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
index 2879dd9..9efd0fc 100644
--- a/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
+++ b/node_modules/@openedx/frontend-build/config/webpack.prod.config.js
@@ -12,6 +12,7 @@ const NewRelicSourceMapPlugin = require('@edx/new-relic-source-map-webpack-plugi
const HtmlWebpackPlugin = require('html-webpack-plugin');
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
const path = require('path');
+const fs = require('fs');
const PostCssAutoprefixerPlugin = require('autoprefixer');
const PostCssRTLCSS = require('postcss-rtlcss');
const PostCssCustomMediaCSS = require('postcss-custom-media');
@@ -23,6 +24,23 @@ const HtmlWebpackNewRelicPlugin = require('../lib/plugins/html-webpack-new-relic
const commonConfig = require('./webpack.common.config');
const presets = require('../lib/presets');
+/**
+ * This condition confirms whether the configuration for the MFE has switched to a JS-based configuration
+ * as previously implemented in frontend-build and frontend-platform. If the environment variable JS_CONFIG_FILEPATH
+ * exists, then an env.config.js(x) file will be copied from the location referenced by the environment variable to the
+ * root directory. Its env variables can be accessed with getConfig().
+ *
+ * https://github.com/openedx/frontend-build/blob/master/docs/0002-js-environment-config.md
+ * https://github.com/openedx/frontend-platform/blob/master/docs/decisions/0007-javascript-file-configuration.rst
+ */
+
+const envConfigPath = process.env.JS_CONFIG_FILEPATH;
+
+if (envConfigPath) {
+ const envConfigFilename = envConfigPath.slice(envConfigPath.indexOf('env.config'));
+ fs.copyFileSync(envConfigPath, envConfigFilename);
+}
+
// Add process env vars. Currently used only for setting the PUBLIC_PATH.
dotenv.config({
path: path.resolve(process.cwd(), '.env'),

View File

@@ -8,7 +8,7 @@
"rebaseStalePrs": true, "rebaseStalePrs": true,
"packageRules": [ "packageRules": [
{ {
"matchPackagePatterns": ["@edx", "@openedx"], "matchPackagePatterns": ["@edx"],
"matchUpdateTypes": ["minor", "patch"], "matchUpdateTypes": ["minor", "patch"],
"automerge": true "automerge": true
} }

View File

@@ -3,8 +3,8 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { import {
FormattedMessage, FormattedDate, injectIntl, intlShape, FormattedMessage, FormattedDate, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Alert, Hyperlink } from '@openedx/paragon'; import { Alert, Hyperlink } from '@edx/paragon';
import { Info } from '@openedx/paragon/icons'; import { Info } from '@edx/paragon/icons';
import messages from './messages'; import messages from './messages';

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n'; import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@openedx/paragon'; import { PageBanner } from '@edx/paragon';
const AccessExpirationMasqueradeBanner = ({ payload }) => { const AccessExpirationMasqueradeBanner = ({ payload }) => {
const { const {

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert, Hyperlink } from '@openedx/paragon'; import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@openedx/paragon/icons'; import { WarningFilled } from '@edx/paragon/icons';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import genericMessages from './messages'; import genericMessages from './messages';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime, FormattedRelativeTime,
FormattedTime, FormattedTime,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert } from '@edx/paragon';
import { Info } from '@openedx/paragon/icons'; import { Info } from '@edx/paragon/icons';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';

View File

@@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n'; import { FormattedMessage, FormattedDate } from '@edx/frontend-platform/i18n';
import { PageBanner } from '@openedx/paragon'; import { PageBanner } from '@edx/paragon';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';

View File

@@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Alert, Button } from '@openedx/paragon'; import { Alert, Button } from '@edx/paragon';
import { Info, WarningFilled } from '@openedx/paragon/icons'; import { Info, WarningFilled } from '@edx/paragon/icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

@@ -7,8 +7,8 @@ import {
Button, Button,
Spinner, Spinner,
Icon, Icon,
} from '@openedx/paragon'; } from '@edx/paragon';
import { Check, ArrowForward } from '@openedx/paragon/icons'; import { Check, ArrowForward } from '@edx/paragon/icons';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { sendActivationEmail } from '../../courseware/data'; import { sendActivationEmail } from '../../courseware/data';
import messages from './messages'; import messages from './messages';

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth'; import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Hyperlink } from '@openedx/paragon'; import { Alert, Hyperlink } from '@edx/paragon';
import { WarningFilled } from '@openedx/paragon/icons'; import { WarningFilled } from '@edx/paragon/icons';
import genericMessages from '../../generic/messages'; import genericMessages from '../../generic/messages';

33
src/constants.js Normal file
View File

@@ -0,0 +1,33 @@
export const DECODE_ROUTES = {
ACCESS_DENIED: '/course/:courseId/access-denied',
HOME: '/course/:courseId/home',
LIVE: '/course/:courseId/live',
DATES: '/course/:courseId/dates',
DISCUSSION: '/course/:courseId/discussion/:path/*',
PROGRESS: [
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
],
COURSE_END: '/course/:courseId/course-end',
COURSEWARE: [
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
],
REDIRECT_HOME: 'home/:courseId',
REDIRECT_SURVEY: 'survey/:courseId',
};
export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
CONSENT: 'consent',
};
export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect',
CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
};

View File

@@ -1,58 +0,0 @@
export const DECODE_ROUTES = {
ACCESS_DENIED: '/course/:courseId/access-denied',
HOME: '/course/:courseId/home',
LIVE: '/course/:courseId/live',
DATES: '/course/:courseId/dates',
DISCUSSION: '/course/:courseId/discussion/:path/*',
PROGRESS: [
'/course/:courseId/progress/:targetUserId/',
'/course/:courseId/progress',
],
COURSE_END: '/course/:courseId/course-end',
COURSEWARE: [
'/course/:courseId/:sequenceId/:unitId',
'/course/:courseId/:sequenceId',
'/course/:courseId',
],
REDIRECT_HOME: 'home/:courseId',
REDIRECT_SURVEY: 'survey/:courseId',
} as const satisfies Readonly<{ [k: string]: string | readonly string[] }>;
export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',
CONSENT: 'consent',
} as const satisfies Readonly<{ [k: string]: string }>;
export const REDIRECT_MODES = {
DASHBOARD_REDIRECT: 'dashboard-redirect',
ENTERPRISE_LEARNER_DASHBOARD_REDIRECT: 'enterprise-learner-dashboard-redirect',
CONSENT_REDIRECT: 'consent-redirect',
HOME_REDIRECT: 'home-redirect',
SURVEY_REDIRECT: 'survey-redirect',
} as const satisfies Readonly<{ [k: string]: string }>;
export const VERIFIED_MODES = [
'professional',
'verified',
'no-id-professional',
'credit',
'masters',
'executive-education',
'paid-executive-education',
'paid-bootcamp',
] as const satisfies readonly string[];
export const WIDGETS = {
DISCUSSIONS: 'DISCUSSIONS',
NOTIFICATIONS: 'NOTIFICATIONS',
} as const satisfies Readonly<{ [k: string]: string }>;
export const LOADING = 'loading';
export const LOADED = 'loaded';
export const FAILED = 'failed';
export const DENIED = 'denied';
export type StatusValue = typeof LOADING | typeof LOADED | typeof FAILED | typeof DENIED;

View File

@@ -1,72 +1,54 @@
import React, { useMemo } from 'react'; import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Tabs, Tab } from '@openedx/paragon'; import { Tabs, Tab } from '@edx/paragon';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import CoursewareSearchResults from './CoursewareSearchResults'; import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages'; import messages from './messages';
import { useCoursewareSearchParams } from './hooks';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
const filterAll = 'all'; const noFilterKey = 'none';
const filterTypes = ['text', 'video', 'sequence']; const noFilterLabel = 'All content';
const filterOther = 'other';
const validFilters = [filterAll, ...filterTypes, filterOther]; export const filteredResultsBySelection = ({ key = noFilterKey, results = [] }) => (
key === noFilterKey ? results : results.filter(({ type }) => type === key)
);
export const CoursewareSearchResultsFilter = ({ intl }) => { export const CoursewareSearchResultsFilter = ({ intl }) => {
const { courseId } = useParams(); const { courseId } = useParams();
const lastSearch = useModel('contentSearchResults', courseId); const lastSearch = useModel('contentSearchResults', courseId);
const { filter: filterKeyword, setFilter } = useCoursewareSearchParams();
if (!lastSearch) { return null; } if (!lastSearch || !lastSearch?.results?.length) { return null; }
const { results: data = [] } = lastSearch; const { total, results } = lastSearch;
// If there's no data, we show an empty result. const filters = [
if (!data.length) { return <CoursewareSearchResults />; } {
key: noFilterKey,
label: noFilterLabel,
count: total,
},
...lastSearch.filters,
];
const results = useMemo(() => { const getFilterTitle = (key, fallback) => {
// This reducer distributes the data into different groups to make it easy to const msg = messages[`filter:${key}`];
// use on the filters. if (!msg) { return fallback; }
// All results are added to the "all" key and then to its proper group key as well. return intl.formatMessage(msg);
const grouped = data.reduce((acc, { type, ...rest }) => { };
const resultType = filterTypes.includes(type) ? type : filterOther;
acc[filterAll].push({ type: resultType, ...rest });
acc[resultType] = [...(acc[resultType] || []), { type: resultType, ...rest }];
return acc;
}, { [filterAll]: [] });
// This is just to format the output object with the expected tab order.
const output = {};
validFilters.forEach(key => { if (grouped[key]) { output[key] = grouped[key]; } });
return output;
}, [lastSearch]);
const tabKeys = Object.keys(results);
// Filter has no use if it has only 2 tabs (The "all" tab and another one with the same items).
if (tabKeys.length < 3) { return <CoursewareSearchResults results={results[filterAll]} />; }
const filters = useMemo(() => tabKeys.map((key) => ({
key,
label: intl.formatMessage(messages[`filter:${key}`]),
count: results[key].length,
})), [results]);
const activeKey = validFilters.includes(filterKeyword) ? filterKeyword : filterAll;
return ( return (
<Tabs <Tabs
id="courseware-search-results-tabs" id="courseware-search-results-tabs"
className="courseware-search-results-tabs"
data-testid="courseware-search-results-tabs" data-testid="courseware-search-results-tabs"
variant="tabs" variant="tabs"
activeKey={activeKey} defaultActiveKey={noFilterKey}
onSelect={setFilter}
> >
{filters.filter(({ count }) => (count > 0)).map(({ key, label }) => ( {filters.map(({ key, label }) => (
<Tab key={key} eventKey={key} title={label} data-testid={`courseware-search-results-tabs-${key}`}> <Tab key={key} eventKey={key} title={getFilterTitle(key, label)}>
<CoursewareSearchResults results={results[key]} /> <CoursewareSearchResults
results={filteredResultsBySelection({ key, results })}
/>
</Tab> </Tab>
))} ))}
</Tabs> </Tabs>

View File

@@ -8,22 +8,34 @@ import {
screen, screen,
waitFor, waitFor,
} from '../../setupTest'; } from '../../setupTest';
import { CoursewareSearchResultsFilter } from './CoursewareResultsFilter'; import { CoursewareSearchResultsFilter, filteredResultsBySelection } from './CoursewareResultsFilter';
import { useCoursewareSearchParams } from './hooks';
import initializeStore from '../../store'; import initializeStore from '../../store';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
import searchResultsFactory from './test-data/search-results-factory';
jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({ jest.mock('../../generic/model-store', () => ({
useModel: jest.fn(), useModel: jest.fn(),
})); }));
global.ResizeObserver = jest.fn().mockImplementation(() => ({ const mockResults = [
observe: jest.fn(), {
unobserve: jest.fn(), id: 'video-1', type: 'video', title: 'video_title', score: 3, contentHits: 1, url: '/video-1', location: ['path1', 'path2'],
disconnect: jest.fn(), },
})); {
id: 'video-2', type: 'video', title: 'video_title2', score: 2, contentHits: 1, url: '/video-2', location: ['path1', 'path2'],
},
{
id: 'document-1', type: 'document', title: 'document_title', score: 3, contentHits: 1, url: '/document-1', location: ['path1', 'path2'],
},
{
id: 'text-1', type: 'text', title: 'text_title1', score: 3, contentHits: 1, url: '/text-1', location: ['path1', 'path2'],
},
{
id: 'text-2', type: 'text', title: 'text_title2', score: 2, contentHits: 1, url: '/text-2', location: ['path1', 'path2'],
},
{
id: 'text-3', type: 'text', title: 'text_title3', score: 1, contentHits: 1, url: '/text-3', location: ['path1', 'path2'],
},
];
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course'; const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'; const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
@@ -34,14 +46,6 @@ const intl = {
formatMessage: (message) => message?.defaultMessage || '', formatMessage: (message) => message?.defaultMessage || '',
}; };
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
function renderComponent(props = {}) { function renderComponent(props = {}) {
const store = initializeStore(); const store = initializeStore();
history.push(pathname); history.push(pathname);
@@ -58,75 +62,56 @@ function renderComponent(props = {}) {
describe('CoursewareSearchResultsFilter', () => { describe('CoursewareSearchResultsFilter', () => {
beforeAll(initializeMockApp); beforeAll(initializeMockApp);
beforeEach(() => { describe('filteredResultsBySelection', () => {
useCoursewareSearchParams.mockReturnValue(coursewareSearch); it('returns a no values array when no results are provided', () => {
const results = filteredResultsBySelection({ results: [] });
expect(results.length).toEqual(0);
});
it('returns all values when no key value is provided', () => {
const results = filteredResultsBySelection({ results: mockResults });
expect(results.length).toEqual(mockResults.length);
});
it('returns only "video"-typed elements when the key value "video" is given', () => {
const results = filteredResultsBySelection({ key: 'video', results: mockResults });
expect(results.length).toEqual(2);
});
it('returns only "course_outline"-typed elements when the key value "document" is given', () => {
const results = filteredResultsBySelection({ key: 'document', results: mockResults });
expect(results.length).toEqual(1);
});
it('returns only "text"-typed elements when the key value "text" is given', () => {
const results = filteredResultsBySelection({ key: 'text', results: mockResults });
expect(results.length).toEqual(3);
});
}); });
afterEach(() => { describe('</CoursewareSearchResultsFilter />', () => {
jest.clearAllMocks();
});
describe('when returning full results', () => {
beforeEach(() => { beforeEach(() => {
useModel.mockReturnValue(searchResultsFactory()); jest.clearAllMocks();
renderComponent();
}); });
it('should render without errors', async () => { it('should render', async () => {
await waitFor(() => { useModel.mockReturnValue({
expect(useCoursewareSearchParams).toBeCalled(); total: 6,
}); results: mockResults,
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-all')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-text')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-video')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-sequence')).toBeInTheDocument();
expect(screen.queryByTestId('courseware-search-results-tabs-other')).toBeInTheDocument();
});
});
describe('when returning only one result type', () => {
beforeEach(async () => {
// Get results for only videos
const data = searchResultsFactory();
const onlyVideos = data.results.filter(({ type }) => type === 'video');
const filteredResults = {
...data,
results: onlyVideos,
};
useModel.mockReturnValue(filteredResults);
await renderComponent();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
});
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument();
});
});
describe('when there are not results', () => {
beforeEach(async () => {
useModel.mockReturnValue(searchResultsFactory('blah', {
results: [],
filters: [], filters: [],
total: 0,
maxScore: null,
ms: 5,
}));
await renderComponent();
});
it('should not render', async () => {
await waitFor(() => {
expect(useCoursewareSearchParams).toBeCalled();
}); });
expect(screen.queryByTestId('courseware-search-results-tabs')).not.toBeInTheDocument(); await renderComponent();
await waitFor(() => {
expect(screen.queryByTestId('courseware-search-results-tabs')).toBeInTheDocument();
expect(screen.queryByText(/All content/)).toBeInTheDocument();
});
}); });
}); });
}); });

View File

@@ -1,16 +1,16 @@
import React, { useEffect } from 'react'; import React, { useState } from 'react';
import { useParams } from 'react-router'; import { useParams } from 'react-router';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics'; import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { import {
Alert, Button, Icon, Spinner, Alert, Button, Icon, Spinner,
} from '@openedx/paragon'; } from '@edx/paragon';
import { import {
Close, Close,
} from '@openedx/paragon/icons'; } from '@edx/paragon/icons';
import { setShowSearch } from '../data/slice'; import { setShowSearch } from '../data/slice';
import { useCoursewareSearchParams, useElementBoundingBox, useLockScroll } from './hooks'; import { useElementBoundingBox, useLockScroll } from './hooks';
import messages from './messages'; import messages from './messages';
import CoursewareSearchForm from './CoursewareSearchForm'; import CoursewareSearchForm from './CoursewareSearchForm';
@@ -20,7 +20,6 @@ import { searchCourseContent } from '../data/thunks';
const CoursewareSearch = ({ intl, ...sectionProps }) => { const CoursewareSearch = ({ intl, ...sectionProps }) => {
const { courseId } = useParams(); const { courseId } = useParams();
const { query: searchKeyword, setQuery, clearSearchParams } = useCoursewareSearchParams();
const dispatch = useDispatch(); const dispatch = useDispatch();
const { org } = useModel('courseHomeMeta', courseId); const { org } = useModel('courseHomeMeta', courseId);
const { const {
@@ -29,6 +28,7 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
errors, errors,
total, total,
} = useModel('contentSearchResults', courseId); } = useModel('contentSearchResults', courseId);
const [searchKeyword, setSearchKeyword] = useState(lastSearchKeyword);
useLockScroll(); useLockScroll();
@@ -36,7 +36,6 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
const top = info ? `${Math.floor(info.top)}px` : 0; const top = info ? `${Math.floor(info.top)}px` : 0;
const clearSearch = () => { const clearSearch = () => {
clearSearchParams();
dispatch(updateModel({ dispatch(updateModel({
modelType: 'contentSearchResults', modelType: 'contentSearchResults',
model: { model: {
@@ -49,35 +48,34 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
})); }));
}; };
const handleSubmit = (value) => { const handleSubmit = () => {
if (!value) { if (!searchKeyword) {
clearSearch(); clearSearch();
return; return;
} }
sendTrackingLogEvent('edx.course.home.courseware_search.submit', { const eventProperties = {
org_key: org, org_key: org,
courserun_key: courseId, courserun_key: courseId,
};
sendTrackingLogEvent('edx.course.home.courseware_search.submit', {
...eventProperties,
event_type: 'searchKeyword', event_type: 'searchKeyword',
keyword: value, keyword: searchKeyword,
}); });
dispatch(searchCourseContent(courseId, value)); dispatch(searchCourseContent(courseId, searchKeyword));
setQuery(value);
}; };
useEffect(() => {
handleSubmit(searchKeyword);
}, []);
const handleOnChange = (value) => { const handleOnChange = (value) => {
if (value === searchKeyword) { return; } if (value === searchKeyword) { return; }
if (!value) { clearSearch(); }
};
const handleSearchCloseClick = () => { setSearchKeyword(value);
clearSearch();
dispatch(setShowSearch(false)); if (!value) {
clearSearch();
}
}; };
let status = 'idle'; let status = 'idle';
@@ -96,14 +94,14 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
variant="tertiary" variant="tertiary"
className="p-1" className="p-1"
aria-label={intl.formatMessage(messages.searchCloseAction)} aria-label={intl.formatMessage(messages.searchCloseAction)}
onClick={handleSearchCloseClick} onClick={() => dispatch(setShowSearch(false))}
data-testid="courseware-search-close-button" data-testid="courseware-search-close-button"
><Icon src={Close} /> ><Icon src={Close} />
</Button> </Button>
</div> </div>
<div className="courseware-search__outer-content"> <div className="courseware-search__outer-content">
<div className="courseware-search__content"> <div className="courseware-search__content">
<h1 className="h2">{intl.formatMessage(messages.searchModuleTitle)}</h1> <h2>{intl.formatMessage(messages.searchModuleTitle)}</h2>
<CoursewareSearchForm <CoursewareSearchForm
searchTerm={searchKeyword} searchTerm={searchKeyword}
onSubmit={handleSubmit} onSubmit={handleSubmit}
@@ -111,27 +109,27 @@ const CoursewareSearch = ({ intl, ...sectionProps }) => {
placeholder={intl.formatMessage(messages.searchBarPlaceholderText)} placeholder={intl.formatMessage(messages.searchBarPlaceholderText)}
/> />
{status === 'loading' ? ( {status === 'loading' ? (
<div className="courseware-search__spinner" data-testid="courseware-search-spinner"> <div className="courseware-search__spinner">
<Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} /> <Spinner animation="border" variant="light" screenReaderText={intl.formatMessage(messages.loading)} />
</div> </div>
) : null} ) : null}
{status === 'error' && ( {status === 'error' && (
<Alert className="mt-4" variant="danger" data-testid="courseware-search-error"> <Alert className="mt-4" variant="danger">
{intl.formatMessage(messages.searchResultsError)} {intl.formatMessage(messages.searchResultsError)}
</Alert> </Alert>
)} )}
{status === 'results' ? ( {status === 'results' ? (
<> <>
{total > 0 ? ( <div className="courseware-search__results-summary">{total > 0
<div ? (
className="courseware-search__results-summary" intl.formatMessage(
aria-live="polite" total === 1
aria-relevant="all" ? messages.searchResultsSingular
aria-atomic="true" : messages.searchResultsPlural,
data-testid="courseware-search-summary" { total, keyword: lastSearchKeyword },
>{intl.formatMessage(messages.searchResultsLabel, { total, keyword: lastSearchKeyword })} )
</div> ) : intl.formatMessage(messages.searchResultsNone)}
) : null} </div>
<CoursewareSearchResultsFilterContainer /> <CoursewareSearchResultsFilterContainer />
</> </>
) : null} ) : null}

View File

@@ -2,45 +2,21 @@ import React from 'react';
import { history } from '@edx/frontend-platform'; import { history } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react'; import { AppProvider } from '@edx/frontend-platform/react';
import { Route, Routes } from 'react-router-dom'; import { Route, Routes } from 'react-router-dom';
import { sendTrackingLogEvent } from '@edx/frontend-platform/analytics';
import { import {
initializeMockApp, initializeMockApp,
render, render,
screen, screen,
waitFor,
fireEvent,
} from '../../setupTest'; } from '../../setupTest';
import { CoursewareSearch } from './index'; import { CoursewareSearch } from './index';
import { useElementBoundingBox, useLockScroll, useCoursewareSearchParams } from './hooks'; import { useElementBoundingBox, useLockScroll } from './hooks';
import initializeStore from '../../store'; import initializeStore from '../../store';
import { searchCourseContent } from '../data/thunks'; import { useModel } from '../../generic/model-store';
import { setShowSearch } from '../data/slice';
import { updateModel, useModel } from '../../generic/model-store';
jest.mock('./hooks'); jest.mock('./hooks');
jest.mock('../../generic/model-store', () => ({ jest.mock('../../generic/model-store', () => ({
updateModel: jest.fn(),
useModel: jest.fn(), useModel: jest.fn(),
})); }));
jest.mock('@edx/frontend-platform/analytics', () => ({
sendTrackingLogEvent: jest.fn(),
}));
jest.mock('../data/thunks', () => ({
searchCourseContent: jest.fn(),
}));
jest.mock('../data/slice', () => ({
setShowSearch: jest.fn(),
}));
const mockDispatch = jest.fn();
jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch,
}));
const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course'; const decodedCourseId = 'course-v1:edX+DemoX+Demo_Course';
const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction'; const decodedSequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction';
const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc'; const decodedUnitId = 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@vertical_0270f6de40fc';
@@ -56,14 +32,6 @@ const defaultProps = {
total: 0, total: 0,
}; };
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const intl = { const intl = {
formatMessage: (message) => message?.defaultMessage || '', formatMessage: (message) => message?.defaultMessage || '',
}; };
@@ -82,22 +50,7 @@ function renderComponent(props = {}) {
} }
const mockModels = ((props = defaultProps) => { const mockModels = ((props = defaultProps) => {
useModel.mockReturnValue({ useModel.mockReturnValue(props);
...defaultProps,
...props,
});
updateModel.mockReturnValue({
type: 'MOCK_ACTION',
payload: {
modelType: 'contentSearchResults',
model: defaultProps,
},
});
});
const mockSearchParams = ((props = coursewareSearch) => {
useCoursewareSearchParams.mockReturnValue(props);
}); });
describe('CoursewareSearch', () => { describe('CoursewareSearch', () => {
@@ -112,18 +65,16 @@ describe('CoursewareSearch', () => {
useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition })); useElementBoundingBox.mockImplementation(() => ({ top: tabsTopPosition }));
}); });
it('should use useElementBoundingBox() and useLockScroll() hooks', () => { it('Should use useElementBoundingBox() and useLockScroll() hooks', () => {
mockModels(); mockModels();
mockSearchParams();
renderComponent(); renderComponent();
expect(useElementBoundingBox).toBeCalledTimes(1); expect(useElementBoundingBox).toBeCalledTimes(1);
expect(useLockScroll).toBeCalledTimes(1); expect(useLockScroll).toBeCalledTimes(1);
}); });
it('should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => { it('Should have a "--modal-top-position" CSS variable matching the CourseTabsNavigation top position', () => {
mockModels(); mockModels();
mockSearchParams();
renderComponent(); renderComponent();
const section = screen.getByTestId('courseware-search-section'); const section = screen.getByTestId('courseware-search-section');
@@ -131,26 +82,11 @@ describe('CoursewareSearch', () => {
}); });
}); });
describe('when clicking on the "Close" button', () => {
it('should dispatch setShowSearch(false)', async () => {
mockModels();
renderComponent();
await waitFor(() => {
const close = screen.queryByTestId('courseware-search-close-button');
fireEvent.click(close);
});
expect(setShowSearch).toBeCalledWith(false);
});
});
describe('when CourseTabsNavigation is not present', () => { describe('when CourseTabsNavigation is not present', () => {
it('should use "--modal-top-position: 0" if nce element is not present', () => { it('Should use "--modal-top-position: 0" if nce element is not present', () => {
useElementBoundingBox.mockImplementation(() => undefined); useElementBoundingBox.mockImplementation(() => undefined);
mockModels(); mockModels();
mockSearchParams();
renderComponent(); renderComponent();
const section = screen.getByTestId('courseware-search-section'); const section = screen.getByTestId('courseware-search-section');
@@ -159,127 +95,12 @@ describe('CoursewareSearch', () => {
}); });
describe('when passing extra props', () => { describe('when passing extra props', () => {
it('should pass on extra props to section element', () => { it('Should pass on extra props to section element', () => {
mockModels(); mockModels();
mockSearchParams();
renderComponent({ foo: 'bar' }); renderComponent({ foo: 'bar' });
const section = screen.getByTestId('courseware-search-section'); const section = screen.getByTestId('courseware-search-section');
expect(section).toHaveAttribute('foo', 'bar'); expect(section).toHaveAttribute('foo', 'bar');
}); });
}); });
describe('when submitting an empty search', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels();
renderComponent();
await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});
expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});
describe('when submitting a search', () => {
it('should show a loading state', () => {
mockModels({
loading: true,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-spinner')).toBeInTheDocument();
});
it('should call searchCourseContent', async () => {
mockModels();
renderComponent();
const searchKeyword = 'course';
await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: searchKeyword } });
});
await waitFor(() => {
const submit = screen.queryByTestId('courseware-search-form-submit');
fireEvent.click(submit);
});
expect(sendTrackingLogEvent).toHaveBeenCalledWith('edx.course.home.courseware_search.submit', {
org_key: defaultProps.org,
courserun_key: decodedCourseId,
event_type: 'searchKeyword',
keyword: searchKeyword,
});
expect(searchCourseContent).toHaveBeenCalledWith(decodedCourseId, searchKeyword);
});
it('should show an error state if any', () => {
mockModels({
errors: ['foo'],
});
renderComponent();
expect(screen.queryByTestId('courseware-search-error')).toBeInTheDocument();
});
it('should not show a summary if there are no results', () => {
mockModels({
searchKeyword: 'test',
total: 0,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-summary')).not.toBeInTheDocument();
});
it('should show a summary for the results', () => {
mockModels({
searchKeyword: 'fubar',
total: 1,
});
renderComponent();
expect(screen.queryByTestId('courseware-search-summary').textContent).toBe('Results for "fubar":');
});
});
describe('when clearing the search input', () => {
it('should clear the search by dispatch updateModel', async () => {
mockModels({
searchKeyword: 'fubar',
total: 2,
});
renderComponent();
await waitFor(() => {
const input = screen.queryByTestId('courseware-search-form').querySelector('input');
fireEvent.change(input, { target: { value: '' } });
});
expect(updateModel).toHaveBeenCalledWith({
modelType: 'contentSearchResults',
model: {
id: decodedCourseId,
searchKeyword: '',
results: [],
errors: undefined,
loading: false,
},
});
});
});
}); });

View File

@@ -1,24 +0,0 @@
import React from 'react';
import {
initializeMockApp,
render,
screen,
} from '../../setupTest';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
function renderComponent() {
const { container } = render(<CoursewareSearchEmpty />);
return container;
}
describe('CoursewareSearchEmpty', () => {
beforeAll(async () => {
initializeMockApp();
});
it('should match the snapshot', () => {
renderComponent();
expect(screen.getByTestId('no-results')).toMatchSnapshot();
});
});

View File

@@ -1,11 +1,8 @@
import React from 'react'; import React from 'react';
import { SearchField } from '@edx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { SearchField } from '@openedx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages';
const CoursewareSearchForm = ({ const CoursewareSearchForm = ({
intl,
searchTerm, searchTerm,
onSubmit, onSubmit,
onChange, onChange,
@@ -17,27 +14,17 @@ const CoursewareSearchForm = ({
onChange={onChange} onChange={onChange}
submitButtonLocation="external" submitButtonLocation="external"
className="courseware-search-form" className="courseware-search-form"
screenReaderText={{
label: intl.formatMessage(messages.searchSubmitLabel),
clearButton: intl.formatMessage(messages.searchClearAction),
submitButton: null, // Remove the sr-only label in the button.
}}
> >
<div className="pgn__searchfield_wrapper" data-testid="courseware-search-form"> <div className="pgn__searchfield_wrapper" data-testid="courseware-search-form">
<SearchField.Label /> <SearchField.Label />
<SearchField.Input placeholder={placeholder} autoFocus /> <SearchField.Input placeholder={placeholder} autoFocus />
<SearchField.ClearButton /> <SearchField.ClearButton />
</div> </div>
<SearchField.SubmitButton <SearchField.SubmitButton submitButtonLocation="external" />
buttonText={intl.formatMessage(messages.searchSubmitLabel)}
submitButtonLocation="external"
data-testid="courseware-search-form-submit"
/>
</SearchField.Advanced> </SearchField.Advanced>
); );
CoursewareSearchForm.propTypes = { CoursewareSearchForm.propTypes = {
intl: intlShape.isRequired,
searchTerm: PropTypes.string, searchTerm: PropTypes.string,
onSubmit: PropTypes.func, onSubmit: PropTypes.func,
onChange: PropTypes.func, onChange: PropTypes.func,
@@ -51,4 +38,4 @@ CoursewareSearchForm.defaultProps = {
placeholder: undefined, placeholder: undefined,
}; };
export default injectIntl(CoursewareSearchForm); export default CoursewareSearchForm;

View File

@@ -47,8 +47,8 @@ describe('CoursewareSearchToggle', () => {
it('should call onSubmit handler when submit is clicked', async () => { it('should call onSubmit handler when submit is clicked', async () => {
await act(async () => renderComponent(placeholderText, onSubmitHandlerMock, onChangeHandlerMock)); await act(async () => renderComponent(placeholderText, onSubmitHandlerMock, onChangeHandlerMock));
await waitFor(async () => { await waitFor(() => {
const element = await screen.findByTestId('courseware-search-form-submit'); const element = screen.queryAllByText('Search')[0];
fireEvent.click(element); fireEvent.click(element);
expect(onSubmitHandlerMock).toHaveBeenCalledTimes(1); expect(onSubmitHandlerMock).toHaveBeenCalledTimes(1);
}); });

View File

@@ -1,22 +1,21 @@
import React from 'react'; import React from 'react';
import { import {
Folder, TextFields, VideoCamera, Article, Folder, TextFields, VideoCamera, Article,
} from '@openedx/paragon/icons'; } from '@edx/paragon/icons';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { Icon } from '@openedx/paragon'; import { Icon } from '@edx/paragon';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import CoursewareSearchEmpty from './CoursewareSearchEmpty'; import CoursewareSearchEmpty from './CoursewareSearchEmpty';
const iconTypeMapping = { const iconTypeMapping = {
document: Folder,
text: TextFields, text: TextFields,
video: VideoCamera, video: VideoCamera,
sequence: Folder,
other: Article,
}; };
const defaultIcon = Article; const defaultIcon = Article;
const CoursewareSearchResults = ({ results = [] }) => { const CoursewareSearchResults = ({ results }) => {
if (!results?.length) { if (!results?.length) {
return <CoursewareSearchEmpty />; return <CoursewareSearchEmpty />;
} }
@@ -35,7 +34,9 @@ const CoursewareSearchResults = ({ results = [] }) => {
}) => { }) => {
const key = type.toLowerCase(); const key = type.toLowerCase();
const icon = iconTypeMapping[key] || defaultIcon; const icon = iconTypeMapping[key] || defaultIcon;
const isExternal = !url.startsWith('/'); const isExternal = !url.startsWith('/');
const linkProps = isExternal ? { const linkProps = isExternal ? {
href: url, href: url,
target: '_blank', target: '_blank',
@@ -69,13 +70,14 @@ const CoursewareSearchResults = ({ results = [] }) => {
}; };
CoursewareSearchResults.propTypes = { CoursewareSearchResults.propTypes = {
results: PropTypes.arrayOf(PropTypes.shape({ results: PropTypes.arrayOf(PropTypes.objectOf({
id: PropTypes.string, id: PropTypes.string,
title: PropTypes.string, title: PropTypes.string,
type: PropTypes.string, type: PropTypes.string,
location: PropTypes.arrayOf(PropTypes.string), location: PropTypes.arrayOf(PropTypes.string),
url: PropTypes.string, url: PropTypes.string,
contentHits: PropTypes.number, contentHits: PropTypes.number,
score: PropTypes.number,
})), })),
}; };

View File

@@ -6,7 +6,7 @@ import {
} from '../../setupTest'; } from '../../setupTest';
import CoursewareSearchResults from './CoursewareSearchResults'; import CoursewareSearchResults from './CoursewareSearchResults';
import messages from './messages'; import messages from './messages';
import searchResultsFactory from './test-data/search-results-factory'; // import mockedData from './test-data/mockedResults'; // TODO: Update this test.
jest.mock('react-redux'); jest.mock('react-redux');
@@ -28,14 +28,11 @@ describe('CoursewareSearchResults', () => {
}); });
}); });
describe('when list of results is provided', () => { /* describe('when list of results is provided', () => {
beforeEach(() => { beforeEach(() => { renderComponent({ results: mockedData }); });
const { results } = searchResultsFactory('course');
renderComponent({ results });
});
it('should match the snapshot', () => { it('should match the snapshot', () => {
expect(screen.getByTestId('search-results')).toMatchSnapshot(); expect(screen.getByTestId('search-results')).toMatchSnapshot();
}); });
}); }); */
}); });

View File

@@ -1,41 +1,31 @@
import React, { useEffect } from 'react'; import React from 'react';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon'; import { Button, Icon } from '@edx/paragon';
import { ManageSearch } from '@openedx/paragon/icons'; import { Search } from '@edx/paragon/icons';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import messages from './messages';
import { useCoursewareSearchFeatureFlag, useCoursewareSearchParams } from './hooks';
import { setShowSearch } from '../data/slice'; import { setShowSearch } from '../data/slice';
import messages from './messages';
import { useCoursewareSearchFeatureFlag } from './hooks';
const CoursewareSearchToggle = ({ const CoursewareSearchToggle = ({
intl, intl,
}) => { }) => {
const dispatch = useDispatch(); const dispatch = useDispatch();
const enabled = useCoursewareSearchFeatureFlag(); const enabled = useCoursewareSearchFeatureFlag();
const { query } = useCoursewareSearchParams();
const handleSearchOpenClick = () => {
dispatch(setShowSearch(true));
};
useEffect(() => {
if (enabled && !!query) { handleSearchOpenClick(); }
}, [enabled]);
if (!enabled) { return null; } if (!enabled) { return null; }
return ( return (
<div className="courseware-search-toggle"> <div className="courseware-searc-toggle">
<Button <Button
variant="outline-primary" variant="tertiary"
size="sm" size="sm"
className="p-1 mt-2 mr-2" className="p-1 mt-2 mr-2 rounded-lg"
aria-label={intl.formatMessage(messages.searchOpenAction)} aria-label={intl.formatMessage(messages.searchOpenAction)}
onClick={handleSearchOpenClick} onClick={() => dispatch(setShowSearch(true))}
data-testid="courseware-search-open-button" data-testid="courseware-search-open-button"
iconAfter={ManageSearch}
> >
{intl.formatMessage(messages.contentSearchButton)} <Icon src={Search} />
</Button> </Button>
</div> </div>
); );

View File

@@ -12,33 +12,14 @@ import { setShowSearch } from '../data/slice';
import { CoursewareSearchToggle } from './index'; import { CoursewareSearchToggle } from './index';
const mockDispatch = jest.fn(); const mockDispatch = jest.fn();
const mockCoursewareSearchParams = jest.fn();
jest.mock('../data/thunks'); jest.mock('../data/thunks');
jest.mock('../data/slice'); jest.mock('../data/slice');
jest.mock('react-redux', () => ({ jest.mock('react-redux', () => ({
...jest.requireActual('react-redux'), ...jest.requireActual('react-redux'),
useDispatch: () => mockDispatch, useDispatch: () => mockDispatch,
})); }));
jest.mock('./hooks', () => ({
...jest.requireActual('./hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
function renderComponent() { function renderComponent() {
const { container } = render(<CoursewareSearchToggle />); const { container } = render(<CoursewareSearchToggle />);
return container; return container;
@@ -55,7 +36,6 @@ describe('CoursewareSearchToggle', () => {
it('Should not render when the waffle flag is disabled', async () => { it('Should not render when the waffle flag is disabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false })); fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: false }));
mockSearchParams();
await act(async () => renderComponent()); await act(async () => renderComponent());
await waitFor(() => { await waitFor(() => {
@@ -66,8 +46,6 @@ describe('CoursewareSearchToggle', () => {
it('Should render when the waffle flag is enabled', async () => { it('Should render when the waffle flag is enabled', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true })); fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();
await act(async () => renderComponent()); await act(async () => renderComponent());
await waitFor(() => { await waitFor(() => {
@@ -78,8 +56,6 @@ describe('CoursewareSearchToggle', () => {
it('Should dispatch setShowSearch(true) when clicking the search button', async () => { it('Should dispatch setShowSearch(true) when clicking the search button', async () => {
fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true })); fetchCoursewareSearchSettings.mockImplementation(() => Promise.resolve({ enabled: true }));
mockSearchParams();
await act(async () => renderComponent()); await act(async () => renderComponent());
const button = await screen.findByTestId('courseware-search-open-button'); const button = await screen.findByTestId('courseware-search-open-button');
fireEvent.click(button); fireEvent.click(button);

View File

@@ -1,10 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
<p
class="courseware-search-results__empty"
data-testid="no-results"
>
No results found.
</p>
`;

View File

@@ -1,1238 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
<div
class="courseware-search-results"
data-testid="search-results"
>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Demo Course Overview
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Passing a Course
</span>
<em>
1
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M10 4H2v16h20V6H12l-2-2z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Passing a Course
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Passing a Course
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Text Input
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Text input
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Pointing on a Picture
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Pointing on a Picture
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Getting Answers
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Getting Answers
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Welcome!
</span>
<em>
30
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Multiple Choice Questions
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Multiple Choice Questions
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Numerical Input
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Homework - Question Styles
</div>
</li>
<li>
<div>
Numerical Input
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M17 10.5V6H3v12h14v-4.5l4 4v-11l-4 4Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Connecting a Circuit and a Circuit Diagram
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
Video Presentation Styles
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
CAPA
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 2: Get Interactive
</div>
</li>
<li>
<div>
Homework - Labs and Demos
</div>
</li>
<li>
<div>
Code Grader
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Interactive Questions
</span>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
Interactive Questions
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Blank HTML Page
</span>
<em>
6
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Introduction
</div>
</li>
<li>
<div>
Demo Course Overview
</div>
</li>
<li>
<div>
Introduction: Video and Sequences
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Discussion Forums
</span>
<em>
5
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Discussion Forums
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Overall Grade
</span>
<em>
7
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
Overall Grade Performance
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Blank HTML Page
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Find Your Study Buddy
</span>
<em>
3
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
<li>
<div>
Homework - Find Your Study Buddy
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
Be Social
</span>
<em>
4
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 3: Be Social
</div>
</li>
<li>
<div>
Lesson 3 - Be Social
</div>
</li>
<li>
<div>
Be Social
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
EdX Exams
</span>
<em>
4
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
About Exams and Certificates
</div>
</li>
<li>
<div>
edX Exams
</div>
</li>
<li>
<div>
EdX Exams
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M2.5 4v3h5v12h3V7h5V4h-13zm19 5h-9v3h3v7h3v-7h3V9z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
When Are Your Exams?
</span>
<em>
2
</em>
</div>
<ul
class="courseware-search-results__breadcrumbs"
>
<li>
<div>
Example Week 1: Getting Started
</div>
</li>
<li>
<div>
Lesson 1 - Getting Started
</div>
</li>
<li>
<div>
When Are Your Exams?
</div>
</li>
</ul>
</div>
</a>
<a
class="courseware-search-results__item"
href="https://www.edx.org"
rel="nofollow"
target="_blank"
>
<div
class="courseware-search-results__icon"
>
<span
class="pgn__icon"
>
<svg
aria-hidden="true"
fill="none"
focusable="false"
height="24"
role="img"
viewBox="0 0 24 24"
width="24"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M3 3v18h18V3H3Zm11 14H7v-2h7v2Zm3-4H7v-2h10v2Zm0-4H7V7h10v2Z"
fill="currentColor"
/>
</svg>
</span>
</div>
<div
class="courseware-search-results__info"
>
<div
class="courseware-search-results__title"
>
<span>
External Course Link Test
</span>
</div>
</div>
</a>
</div>
`;

View File

@@ -1,29 +1,24 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = ` exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
{ Object {
"filters": [ "filters": Array [
{ Object {
"count": 7, "count": 7,
"key": "capa", "key": "capa",
"label": "CAPA", "label": "CAPA",
}, },
{ Object {
"count": 2, "count": 2,
"key": "sequence", "key": "sequence",
"label": "Sequence", "label": "Sequence",
}, },
{ Object {
"count": 9, "count": 9,
"key": "text", "key": "text",
"label": "Text", "label": "Text",
}, },
{ Object {
"count": 1,
"key": "unknown",
"label": "Unknown",
},
{
"count": 2, "count": 2,
"key": "video", "key": "video",
"label": "Video", "label": "Video",
@@ -31,11 +26,11 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
], ],
"maxScore": 3.4545178, "maxScore": 3.4545178,
"ms": 5, "ms": 5,
"results": [ "results": Array [
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
"location": [ "location": Array [
"Introduction", "Introduction",
"Demo Course Overview", "Demo Course Overview",
], ],
@@ -44,10 +39,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "sequence", "type": "sequence",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
"location": [ "location": Array [
"About Exams and Certificates", "About Exams and Certificates",
"edX Exams", "edX Exams",
"Passing a Course", "Passing a Course",
@@ -57,10 +52,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", "id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
"location": [ "location": Array [
"About Exams and Certificates", "About Exams and Certificates",
"edX Exams", "edX Exams",
"Passing a Course", "Passing a Course",
@@ -70,10 +65,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "sequence", "type": "sequence",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Homework - Question Styles", "Homework - Question Styles",
"Text input", "Text input",
@@ -83,10 +78,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Homework - Question Styles", "Homework - Question Styles",
"Pointing on a Picture", "Pointing on a Picture",
@@ -96,10 +91,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
"location": [ "location": Array [
"About Exams and Certificates", "About Exams and Certificates",
"edX Exams", "edX Exams",
"Getting Answers", "Getting Answers",
@@ -109,10 +104,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
"location": [ "location": Array [
"Introduction", "Introduction",
"Demo Course Overview", "Demo Course Overview",
"Introduction: Video and Sequences", "Introduction: Video and Sequences",
@@ -122,10 +117,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "video", "type": "video",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Homework - Question Styles", "Homework - Question Styles",
"Multiple Choice Questions", "Multiple Choice Questions",
@@ -135,10 +130,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Homework - Question Styles", "Homework - Question Styles",
"Numerical Input", "Numerical Input",
@@ -148,10 +143,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", "id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Lesson 1 - Getting Started", "Lesson 1 - Getting Started",
"Video Presentation Styles", "Video Presentation Styles",
@@ -161,10 +156,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "video", "type": "video",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
"location": [ "location": Array [
"Example Week 2: Get Interactive", "Example Week 2: Get Interactive",
"Homework - Labs and Demos", "Homework - Labs and Demos",
"Code Grader", "Code Grader",
@@ -174,10 +169,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", "id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Lesson 1 - Getting Started", "Lesson 1 - Getting Started",
"Interactive Questions", "Interactive Questions",
@@ -187,10 +182,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "capa", "type": "capa",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
"location": [ "location": Array [
"Introduction", "Introduction",
"Demo Course Overview", "Demo Course Overview",
"Introduction: Video and Sequences", "Introduction: Video and Sequences",
@@ -200,10 +195,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
"location": [ "location": Array [
"Example Week 3: Be Social", "Example Week 3: Be Social",
"Lesson 3 - Be Social", "Lesson 3 - Be Social",
"Discussion Forums", "Discussion Forums",
@@ -213,10 +208,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
"location": [ "location": Array [
"About Exams and Certificates", "About Exams and Certificates",
"edX Exams", "edX Exams",
"Overall Grade Performance", "Overall Grade Performance",
@@ -226,10 +221,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
"location": [ "location": Array [
"Example Week 3: Be Social", "Example Week 3: Be Social",
"Lesson 3 - Be Social", "Lesson 3 - Be Social",
"Homework - Find Your Study Buddy", "Homework - Find Your Study Buddy",
@@ -239,10 +234,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
"location": [ "location": Array [
"Example Week 3: Be Social", "Example Week 3: Be Social",
"Homework - Find Your Study Buddy", "Homework - Find Your Study Buddy",
"Homework - Find Your Study Buddy", "Homework - Find Your Study Buddy",
@@ -252,10 +247,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
"location": [ "location": Array [
"Example Week 3: Be Social", "Example Week 3: Be Social",
"Lesson 3 - Be Social", "Lesson 3 - Be Social",
"Be Social", "Be Social",
@@ -265,10 +260,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
"location": [ "location": Array [
"About Exams and Certificates", "About Exams and Certificates",
"edX Exams", "edX Exams",
"EdX Exams", "EdX Exams",
@@ -278,10 +273,10 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
}, },
{ Object {
"contentHits": 0, "contentHits": 0,
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", "id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
"location": [ "location": Array [
"Example Week 1: Getting Started", "Example Week 1: Getting Started",
"Lesson 1 - Getting Started", "Lesson 1 - Getting Started",
"When Are Your Exams? ", "When Are Your Exams? ",
@@ -291,15 +286,6 @@ exports[`mapSearchResponse when the response is correct should match snapshot 1`
"type": "text", "type": "text",
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf", "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
}, },
{
"contentHits": 0,
"id": "random-element-id",
"location": null,
"score": 0.82610154,
"title": "External Course Link Test",
"type": "unknown",
"url": "https://www.edx.org",
},
], ],
"total": 29, "total": 29,
} }

View File

@@ -6,7 +6,7 @@
right: 0; right: 0;
bottom: 0; bottom: 0;
border-top: 1px solid $light-300; border-top: 1px solid $light-300;
z-index: $zindex-modal; // Bootstrap's z-index layer for Modals. z-index: 200;
&__close { &__close {
position: absolute !important; // For some reason it gets overridden position: absolute !important; // For some reason it gets overridden
@@ -35,7 +35,7 @@
&__results-summary { &__results-summary {
font-size: .9rem; font-size: .9rem;
color: $gray-500; color: $gray-400;
padding: 1rem 0 .5rem; padding: 1rem 0 .5rem;
} }
@@ -51,8 +51,6 @@
&__empty { &__empty {
color: $gray-500; color: $gray-500;
padding: 6rem 0;
text-align: center;
} }
&__item { &__item {
@@ -113,7 +111,7 @@
&__breadcrumbs { &__breadcrumbs {
display: flex; display: flex;
gap: 1.25rem; gap: 1.25rem;
color: $gray-500; color: $gray-400;
overflow: hidden; overflow: hidden;
list-style: none; list-style: none;
padding: 0; padding: 0;
@@ -143,14 +141,6 @@
} }
} }
.courseware-search-results-tabs {
border-bottom-color: $gray-400 !important;
&.nav-tabs .nav-link.active {
border-bottom-width: 4px !important;
}
}
@media (min-width: map-get($grid-breakpoints, 'md')) { @media (min-width: map-get($grid-breakpoints, 'md')) {
.courseware-search__content { .courseware-search__content {
padding-top: 8rem; padding-top: 8rem;

View File

@@ -1,5 +1,5 @@
import { useState, useEffect, useLayoutEffect } from 'react'; import { useState, useEffect, useLayoutEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { fetchCoursewareSearchSettings } from '../data/thunks'; import { fetchCoursewareSearchSettings } from '../data/thunks';
@@ -69,19 +69,3 @@ export function useLockScroll() {
}; };
}, []); }, []);
} }
const initSearchParams = { q: '', f: '' };
export function useCoursewareSearchParams() {
const [searchParams, setSearchParams] = useSearchParams(initSearchParams);
const clearSearchParams = () => setSearchParams(initSearchParams);
const query = searchParams.get('q');
const filter = searchParams.get('f')?.toLowerCase();
const setQuery = (q) => setSearchParams((params) => ({ q, f: params.get('f') }));
const setFilter = (f) => setSearchParams((params) => ({ q: params.get('q'), f }));
return {
query, filter, setQuery, setFilter, clearSearchParams,
};
}

View File

@@ -1,13 +1,9 @@
import { renderHook, act } from '@testing-library/react-hooks'; import { renderHook, act } from '@testing-library/react-hooks';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { fetchCoursewareSearchSettings } from '../data/thunks'; import { fetchCoursewareSearchSettings } from '../data/thunks';
import { import {
useCoursewareSearchFeatureFlag, useCoursewareSearchFeatureFlag, useCoursewareSearchState, useElementBoundingBox, useLockScroll,
useCoursewareSearchParams,
useCoursewareSearchState,
useElementBoundingBox,
useLockScroll,
} from './hooks'; } from './hooks';
jest.mock('react-redux'); jest.mock('react-redux');
@@ -188,45 +184,4 @@ describe('CoursewareSearch Hooks', () => {
expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll'); expect(removeBodyClassSpy).toHaveBeenCalledWith('_search-no-scroll');
}); });
}); });
describe('useSearchParams', () => {
const initSearch = { q: '', f: '' };
const q = { value: '' };
const f = { value: '' };
const mockedQuery = { q, f };
const searchParams = { get: (prop) => mockedQuery[prop].value };
const setSearchParams = jest.fn();
beforeEach(() => {
useSearchParams.mockImplementation(() => [searchParams, setSearchParams]);
});
it('should init the search params properly', () => {
const {
query, filter, setQuery, setFilter, clearSearchParams,
} = useCoursewareSearchParams();
expect(useSearchParams).toBeCalledWith(initSearch);
expect(query).toBe('');
expect(filter).toBe('');
setQuery('setQuery');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
setFilter('setFilter');
expect(setSearchParams).toBeCalledWith(expect.any(Function));
clearSearchParams();
expect(setSearchParams).toBeCalledWith(initSearch);
});
it('should return the query and lowercase filter if any', () => {
q.value = '42';
f.value = 'LOWERCASE';
const { query, filter } = useCoursewareSearchParams();
expect(query).toBe('42');
expect(filter).toBe('lowercase');
});
});
}); });

View File

@@ -1,8 +1,8 @@
const Joi = require('joi'); const Joi = require('joi');
const endpointSchema = Joi.object({ const endpointSchema = Joi.object({
took: Joi.number().required(), took: Joi.number(),
total: Joi.number().required(), total: Joi.number(),
maxScore: Joi.number().allow(null), maxScore: Joi.number().allow(null),
results: Joi.array().items(Joi.object({ results: Joi.array().items(Joi.object({
id: Joi.string(), id: Joi.string(),

View File

@@ -19,7 +19,6 @@ describe('mapSearchResponse', () => {
{ key: 'capa', label: 'CAPA', count: 7 }, { key: 'capa', label: 'CAPA', count: 7 },
{ key: 'sequence', label: 'Sequence', count: 2 }, { key: 'sequence', label: 'Sequence', count: 2 },
{ key: 'text', label: 'Text', count: 9 }, { key: 'text', label: 'Text', count: 9 },
{ key: 'unknown', label: 'Unknown', count: 1 },
{ key: 'video', label: 'Video', count: 2 }, { key: 'video', label: 'Video', count: 2 },
]; ];
expect(response.filters).toEqual(expectedFilters); expect(response.filters).toEqual(expectedFilters);

View File

@@ -2,87 +2,67 @@ import { defineMessages } from '@edx/frontend-platform/i18n';
const messages = defineMessages({ const messages = defineMessages({
searchOpenAction: { searchOpenAction: {
id: 'learn.coursewareSearch.openAction', id: 'learn.coursewareSerch.openAction',
defaultMessage: 'Search within this course', defaultMessage: 'Search within this course',
description: 'Aria-label for a button that will pop up Courseware Search.', description: 'Aria-label for a button that will pop up Courseware Search.',
}, },
contentSearchButton: {
id: 'learn.coursewareSearch.contentSearchButton',
defaultMessage: 'Content search',
description: 'Text for a button that will pop up Courseware Search.',
},
searchSubmitLabel: {
id: 'learn.coursewareSearch.submitLabel',
defaultMessage: 'Search',
description: 'Button label that will submit Courseware Search.',
},
searchClearAction: {
id: 'learn.coursewareSearch.clearAction',
defaultMessage: 'Clear search',
description: 'Button label that will the current Courseware Search input.',
},
searchCloseAction: { searchCloseAction: {
id: 'learn.coursewareSearch.closeAction', id: 'learn.coursewareSerch.closeAction',
defaultMessage: 'Close the search form', defaultMessage: 'Close the search form',
description: 'Aria-label for a button that will close Courseware Search.', description: 'Aria-label for a button that will close Courseware Search.',
}, },
searchModuleTitle: { searchModuleTitle: {
id: 'learn.coursewareSearch.searchModuleTitle', id: 'learn.coursewareSerch.searchModuleTitle',
defaultMessage: 'Search this course', defaultMessage: 'Search this course',
description: 'Title for the Courseware Search module.', description: 'Title for the Courseware Search module.',
}, },
searchBarPlaceholderText: { searchBarPlaceholderText: {
id: 'learn.coursewareSearch.searchBarPlaceholderText', id: 'learn.coursewareSerch.searchBarPlaceholderText',
defaultMessage: 'Search', defaultMessage: 'Search',
description: 'Placeholder text for the Courseware Search input control', description: 'Placeholder text for the Courseware Search input control',
}, },
loading: { loading: {
id: 'learn.coursewareSearch.loading', id: 'learn.coursewareSerch.loading',
defaultMessage: 'Searching...', defaultMessage: 'Searching...',
description: 'Screen reader text to use on the spinner while the search is performing.', description: 'Screen reader text to use on the spinner while the search is performing.',
}, },
searchResultsNone: { searchResultsNone: {
id: 'learn.coursewareSearch.searchResultsNone', id: 'learn.coursewareSerch.searchResultsNone',
defaultMessage: 'No results found.', defaultMessage: 'No results found.',
description: 'Text to show when the Courseware Search found no results matching the criteria.', description: 'Text to show when the Courseware Search found no results matching the criteria.',
}, },
searchResultsLabel: { searchResultsSingular: {
id: 'learn.coursewareSearch.searchResultsLabel', id: 'learn.coursewareSerch.searchResultsSingular',
defaultMessage: 'Results for "{keyword}":', defaultMessage: '1 match found for "{keyword}":',
description: 'Text to show above the search results response list.', description: 'Text to show when the Courseware Search found only one result matching the criteria.',
},
searchResultsPlural: {
id: 'learn.coursewareSerch.searchResultsPlural',
defaultMessage: '{total} matches found for "{keyword}":',
description: 'Text to show when the Courseware Search found multiple results matching the criteria.',
}, },
searchResultsError: { searchResultsError: {
id: 'learn.coursewareSearch.searchResultsError', id: 'learn.coursewareSerch.searchResultsError',
defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.', defaultMessage: 'There was an error on the search process. Please try again in a few minutes. If the problem persists, please contact the support team.',
description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.', description: 'Error message to show to the users when there\'s an error with the endpoint or the returned payload format.',
}, },
// These are translations for labeling the filters // These are translations for labeling the filters
'filter:all': { 'filter:none': {
id: 'learn.coursewareSearch.filter:all', id: 'learn.coursewareSerch.filter:none',
defaultMessage: 'All content', defaultMessage: 'All content',
description: 'Label for the search results filter that shows all content (no filter).', description: 'Label for the search results filter that shows all content (no filter).',
}, },
'filter:text': { 'filter:text': {
id: 'learn.coursewareSearch.filter:text', id: 'learn.coursewareSerch.filter:text',
defaultMessage: 'Text', defaultMessage: 'Text',
description: 'Label for the search results filter that shows results with text content.', description: 'Label for the search results filter that shows results with text content.',
}, },
'filter:video': { 'filter:video': {
id: 'learn.coursewareSearch.filter:video', id: 'learn.coursewareSerch.filter:video',
defaultMessage: 'Video', defaultMessage: 'Video',
description: 'Label for the search results filter that shows results with video content.', description: 'Label for the search results filter that shows results with video content.',
}, },
'filter:sequence': {
id: 'learn.coursewareSearch.filter:sequence',
defaultMessage: 'Section',
description: 'Label for the search results filter that shows results with section content.',
},
'filter:other': {
id: 'learn.coursewareSearch.filter:other',
defaultMessage: 'Other',
description: 'Label for the search results filter that shows results with other content.',
},
}); });
export default messages; export default messages;

View File

@@ -542,28 +542,6 @@
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf" "url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf"
}, },
"score": 0.82610154 "score": 0.82610154
},
{
"_index": "courseware_content",
"_type": "_doc",
"_id": "some-external-reference",
"data": {
"course": "External Course",
"org": "edX",
"content": {
"display_name": "External Course Link Test",
"html_content": "This should open a new tab when following the link."
},
"content_type": "Unknown",
"id": "random-element-id",
"start_date": "2013-02-05T05:00:00+00:00",
"content_groups": null,
"course_name": "Demonstration Course",
"location": null,
"excerpt": "Just testing external links",
"url": "https://www.edx.org"
},
"score": 0.82610154
} }
], ],
"access_denied_count": 0 "access_denied_count": 0

View File

@@ -1,17 +0,0 @@
import { camelCaseObject } from '@edx/frontend-platform';
import mockedData from './mocked-response.json';
import mapSearchResponse from '../map-search-response';
function searchResultsFactory(searchKeywords = '', moreInfo = {}) {
const data = camelCaseObject(mockedData);
const info = mapSearchResponse(data, searchKeywords);
const result = {
...info,
...moreInfo,
};
return result;
}
export default searchResultsFactory;

View File

@@ -6,7 +6,6 @@ Factory.define('courseHomeMetadata')
.option('host', 'http://localhost:18000') .option('host', 'http://localhost:18000')
.attrs({ .attrs({
title: 'Demonstration Course', title: 'Demonstration Course',
is_new_discussion_sidebar_view_enabled: false,
is_self_paced: false, is_self_paced: false,
is_enrolled: false, is_enrolled: false,
is_staff: false, is_staff: false,

View File

@@ -1,8 +1,8 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = ` exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
{ Object {
"courseHome": { "courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course", "courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded", "courseStatus": "loaded",
"proctoringPanelStatus": "loading", "proctoringPanelStatus": "loading",
@@ -12,13 +12,9 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"toastBodyText": null, "toastBodyText": null,
"toastHeader": "", "toastHeader": "",
}, },
"courseware": { "courseware": Object {
"courseId": null, "courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading", "courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null, "sequenceId": null,
"sequenceMightBeUnit": false, "sequenceMightBeUnit": false,
"sequenceStatus": "loading", "sequenceStatus": "loading",
@@ -26,12 +22,12 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learningAssistant": ObjectContaining { "learningAssistant": ObjectContaining {
"conversationId": Any<String>, "conversationId": Any<String>,
}, },
"models": { "models": Object {
"courseHomeMeta": { "courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"canViewCertificate": true, "canViewCertificate": true,
"celebrations": null, "celebrations": null,
"courseAccess": { "courseAccess": Object {
"additionalContextUserMessage": null, "additionalContextUserMessage": null,
"developerMessage": null, "developerMessage": null,
"errorCode": null, "errorCode": null,
@@ -42,40 +38,39 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false, "isEnrolled": false,
"isMasquerading": false, "isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false, "isSelfPaced": false,
"isStaff": false, "isStaff": false,
"number": "DemoX", "number": "DemoX",
"org": "edX", "org": "edX",
"originalUserIsStaff": false, "originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z", "start": "2013-02-05T05:00:00Z",
"tabs": [ "tabs": Array [
{ Object {
"slug": "outline", "slug": "outline",
"title": "Course", "title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
}, },
{ Object {
"slug": "discussion", "slug": "discussion",
"title": "Discussion", "title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
}, },
{ Object {
"slug": "wiki", "slug": "wiki",
"title": "Wiki", "title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
}, },
{ Object {
"slug": "progress", "slug": "progress",
"title": "Progress", "title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
}, },
{ Object {
"slug": "instructor", "slug": "instructor",
"title": "Instructor", "title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
}, },
{ Object {
"slug": "dates", "slug": "dates",
"title": "Dates", "title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -84,7 +79,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"title": "Demonstration Course", "title": "Demonstration Course",
"userTimezone": "UTC", "userTimezone": "UTC",
"username": "MockUser", "username": "MockUser",
"verifiedMode": { "verifiedMode": Object {
"accessExpirationDate": null, "accessExpirationDate": null,
"currency": "USD", "currency": "USD",
"currencySymbol": "$", "currencySymbol": "$",
@@ -94,10 +89,10 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
}, },
}, },
}, },
"dates": { "dates": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"courseDateBlocks": [ "courseDateBlocks": Array [
{ Object {
"date": "2020-05-01T17:59:41Z", "date": "2020-05-01T17:59:41Z",
"dateType": "course-start-date", "dateType": "course-start-date",
"description": "", "description": "",
@@ -106,7 +101,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "", "link": "",
"title": "Course Starts", "title": "Course Starts",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"complete": true, "complete": true,
"date": "2020-05-04T02:59:40.942669Z", "date": "2020-05-04T02:59:40.942669Z",
@@ -116,7 +111,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learnerHasAccess": true, "learnerHasAccess": true,
"title": "Multi Badges Completed", "title": "Multi Badges Completed",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2020-05-05T02:59:40.942669Z", "date": "2020-05-05T02:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -125,7 +120,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learnerHasAccess": true, "learnerHasAccess": true,
"title": "Multi Badges Past Due", "title": "Multi Badges Past Due",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z", "date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -135,7 +130,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Past Due 1", "title": "Both Past Due 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z", "date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -145,7 +140,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Past Due 2", "title": "Both Past Due 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"complete": true, "complete": true,
"date": "2020-05-28T08:59:40.942669Z", "date": "2020-05-28T08:59:40.942669Z",
@@ -156,7 +151,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "One Completed/Due 1", "title": "One Completed/Due 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2020-05-28T08:59:40.942669Z", "date": "2020-05-28T08:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -166,7 +161,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "One Completed/Due 2", "title": "One Completed/Due 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"complete": true, "complete": true,
"date": "2020-05-29T08:59:40.942669Z", "date": "2020-05-29T08:59:40.942669Z",
@@ -177,7 +172,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Completed 1", "title": "Both Completed 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"complete": true, "complete": true,
"date": "2020-05-29T08:59:40.942669Z", "date": "2020-05-29T08:59:40.942669Z",
@@ -188,7 +183,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Completed 2", "title": "Both Completed 2",
}, },
{ Object {
"date": "2020-06-16T17:59:40.942669Z", "date": "2020-06-16T17:59:40.942669Z",
"dateType": "verified-upgrade-deadline", "dateType": "verified-upgrade-deadline",
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.", "description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
@@ -197,7 +192,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Upgrade to Verified Certificate", "title": "Upgrade to Verified Certificate",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z", "date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -207,7 +202,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "One Verified 1", "title": "One Verified 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z", "date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -217,7 +212,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "One Verified 2", "title": "One Verified 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z", "date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -227,7 +222,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "ORA Verified 2", "title": "ORA Verified 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z", "date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -237,7 +232,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Verified 1", "title": "Both Verified 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z", "date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -247,7 +242,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "Both Verified 2", "title": "Both Verified 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z", "date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -255,7 +250,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learnerHasAccess": true, "learnerHasAccess": true,
"title": "One Unreleased 1", "title": "One Unreleased 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z", "date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -265,7 +260,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "https://example.com/", "link": "https://example.com/",
"title": "One Unreleased 2", "title": "One Unreleased 2",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z", "date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -274,7 +269,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learnerHasAccess": true, "learnerHasAccess": true,
"title": "Both Unreleased 1", "title": "Both Unreleased 1",
}, },
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z", "date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date", "dateType": "assignment-due-date",
@@ -283,7 +278,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"learnerHasAccess": true, "learnerHasAccess": true,
"title": "Both Unreleased 2", "title": "Both Unreleased 2",
}, },
{ Object {
"date": "2030-08-23T00:00:00Z", "date": "2030-08-23T00:00:00Z",
"dateType": "course-end-date", "dateType": "course-end-date",
"description": "", "description": "",
@@ -292,7 +287,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"link": "", "link": "",
"title": "Course Ends", "title": "Course Ends",
}, },
{ Object {
"date": "2030-09-01T00:00:00Z", "date": "2030-09-01T00:00:00Z",
"dateType": "verification-deadline-date", "dateType": "verification-deadline-date",
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.", "description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
@@ -302,7 +297,7 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
"title": "Verification Deadline", "title": "Verification Deadline",
}, },
], ],
"datesBannerInfo": { "datesBannerInfo": Object {
"contentTypeGatingEnabled": false, "contentTypeGatingEnabled": false,
"missedDeadlines": false, "missedDeadlines": false,
"missedGatedContent": false, "missedGatedContent": false,
@@ -314,75 +309,10 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
}, },
}, },
}, },
"plugins": {}, "recommendations": Object {
"recommendations": {
"recommendationsStatus": "loading", "recommendationsStatus": "loading",
}, },
"specialExams": { "tours": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false, "showCoursewareTour": false,
"showExistingUserCourseHomeTour": false, "showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false, "showNewUserCourseHomeModal": false,
@@ -393,8 +323,8 @@ exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize
`; `;
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = ` exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
{ Object {
"courseHome": { "courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course", "courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded", "courseStatus": "loaded",
"proctoringPanelStatus": "loading", "proctoringPanelStatus": "loading",
@@ -404,13 +334,9 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"toastBodyText": null, "toastBodyText": null,
"toastHeader": "", "toastHeader": "",
}, },
"courseware": { "courseware": Object {
"courseId": null, "courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading", "courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null, "sequenceId": null,
"sequenceMightBeUnit": false, "sequenceMightBeUnit": false,
"sequenceStatus": "loading", "sequenceStatus": "loading",
@@ -418,12 +344,12 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"learningAssistant": ObjectContaining { "learningAssistant": ObjectContaining {
"conversationId": Any<String>, "conversationId": Any<String>,
}, },
"models": { "models": Object {
"courseHomeMeta": { "courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"canViewCertificate": true, "canViewCertificate": true,
"celebrations": null, "celebrations": null,
"courseAccess": { "courseAccess": Object {
"additionalContextUserMessage": null, "additionalContextUserMessage": null,
"developerMessage": null, "developerMessage": null,
"errorCode": null, "errorCode": null,
@@ -434,40 +360,39 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false, "isEnrolled": false,
"isMasquerading": false, "isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false, "isSelfPaced": false,
"isStaff": false, "isStaff": false,
"number": "DemoX", "number": "DemoX",
"org": "edX", "org": "edX",
"originalUserIsStaff": false, "originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z", "start": "2013-02-05T05:00:00Z",
"tabs": [ "tabs": Array [
{ Object {
"slug": "outline", "slug": "outline",
"title": "Course", "title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
}, },
{ Object {
"slug": "discussion", "slug": "discussion",
"title": "Discussion", "title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
}, },
{ Object {
"slug": "wiki", "slug": "wiki",
"title": "Wiki", "title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
}, },
{ Object {
"slug": "progress", "slug": "progress",
"title": "Progress", "title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
}, },
{ Object {
"slug": "instructor", "slug": "instructor",
"title": "Instructor", "title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
}, },
{ Object {
"slug": "dates", "slug": "dates",
"title": "Dates", "title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -476,7 +401,7 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"title": "Demonstration Course", "title": "Demonstration Course",
"userTimezone": "UTC", "userTimezone": "UTC",
"username": "MockUser", "username": "MockUser",
"verifiedMode": { "verifiedMode": Object {
"accessExpirationDate": null, "accessExpirationDate": null,
"currency": "USD", "currency": "USD",
"currencySymbol": "$", "currencySymbol": "$",
@@ -486,80 +411,77 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
}, },
}, },
}, },
"outline": { "outline": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"accessExpiration": null, "accessExpiration": null,
"canShowUpgradeSock": false, "canShowUpgradeSock": false,
"certData": { "certData": Object {
"certStatus": null, "certStatus": null,
"certWebViewUrl": null, "certWebViewUrl": null,
"certificateAvailableDate": null, "certificateAvailableDate": null,
}, },
"courseBlocks": { "courseBlocks": Object {
"courses": { "courses": Object {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": { "block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": Object {
"hasScheduledContent": false, "hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"sectionIds": [ "sectionIds": Array [
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
], ],
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3", "title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
}, },
}, },
"sections": { "sections": Object {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": { "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": Object {
"complete": false, "complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course", "courseId": "course-v1:edX+DemoX+Demo_Course",
"hideFromTOC": undefined,
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"resumeBlock": false, "resumeBlock": false,
"sequenceIds": [ "sequenceIds": Array [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
], ],
"title": "Title of Section", "title": "Title of Section",
}, },
}, },
"sequences": { "sequences": Object {
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": { "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": Object {
"complete": false, "complete": false,
"description": null, "description": null,
"due": null, "due": null,
"effortActivities": 2, "effortActivities": 2,
"effortTime": 15, "effortTime": 15,
"hideFromTOC": undefined,
"icon": null, "icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1", "id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"navigationDisabled": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2", "sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true, "showLink": true,
"title": "Title of Sequence", "title": "Title of Sequence",
}, },
}, },
}, },
"courseGoals": { "courseGoals": Object {
"daysPerWeek": null, "daysPerWeek": null,
"goalOptions": [], "goalOptions": Array [],
"selectedGoal": null, "selectedGoal": null,
"subscribedToReminders": null, "subscribedToReminders": null,
"weeklyLearningGoalEnabled": false, "weeklyLearningGoalEnabled": false,
}, },
"courseTools": [ "courseTools": Array [
{ Object {
"analyticsId": "edx.bookmarks", "analyticsId": "edx.bookmarks",
"title": "Bookmarks", "title": "Bookmarks",
"url": "https://example.com/bookmarks", "url": "https://example.com/bookmarks",
}, },
], ],
"datesBannerInfo": { "datesBannerInfo": Object {
"contentTypeGatingEnabled": false, "contentTypeGatingEnabled": false,
"missedDeadlines": false, "missedDeadlines": false,
"missedGatedContent": false, "missedGatedContent": false,
}, },
"datesWidget": { "datesWidget": Object {
"courseDateBlocks": [], "courseDateBlocks": Array [],
}, },
"enableProctoredExams": undefined, "enableProctoredExams": undefined,
"enrollAlert": { "enrollAlert": Object {
"canEnroll": true, "canEnroll": true,
"extraText": "Contact the administrator.", "extraText": "Contact the administrator.",
}, },
@@ -569,13 +491,13 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
"hasScheduledContent": null, "hasScheduledContent": null,
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"offer": null, "offer": null,
"resumeCourse": { "resumeCourse": Object {
"hasVisitedCourse": false, "hasVisitedCourse": false,
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
}, },
"timeOffsetMillis": 0, "timeOffsetMillis": 0,
"userHasPassingGrade": undefined, "userHasPassingGrade": undefined,
"verifiedMode": { "verifiedMode": Object {
"accessExpirationDate": "2050-01-01T12:00:00", "accessExpirationDate": "2050-01-01T12:00:00",
"currency": "USD", "currency": "USD",
"currencySymbol": "$", "currencySymbol": "$",
@@ -587,75 +509,10 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
}, },
}, },
}, },
"plugins": {}, "recommendations": Object {
"recommendations": {
"recommendationsStatus": "loading", "recommendationsStatus": "loading",
}, },
"specialExams": { "tours": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false, "showCoursewareTour": false,
"showExistingUserCourseHomeTour": false, "showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false, "showNewUserCourseHomeModal": false,
@@ -666,8 +523,8 @@ exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normali
`; `;
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = ` exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
{ Object {
"courseHome": { "courseHome": Object {
"courseId": "course-v1:edX+DemoX+Demo_Course", "courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded", "courseStatus": "loaded",
"proctoringPanelStatus": "loading", "proctoringPanelStatus": "loading",
@@ -677,13 +534,9 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"toastBodyText": null, "toastBodyText": null,
"toastHeader": "", "toastHeader": "",
}, },
"courseware": { "courseware": Object {
"courseId": null, "courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading", "courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null, "sequenceId": null,
"sequenceMightBeUnit": false, "sequenceMightBeUnit": false,
"sequenceStatus": "loading", "sequenceStatus": "loading",
@@ -691,12 +544,12 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"learningAssistant": ObjectContaining { "learningAssistant": ObjectContaining {
"conversationId": Any<String>, "conversationId": Any<String>,
}, },
"models": { "models": Object {
"courseHomeMeta": { "courseHomeMeta": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"canViewCertificate": true, "canViewCertificate": true,
"celebrations": null, "celebrations": null,
"courseAccess": { "courseAccess": Object {
"additionalContextUserMessage": null, "additionalContextUserMessage": null,
"developerMessage": null, "developerMessage": null,
"errorCode": null, "errorCode": null,
@@ -707,40 +560,39 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false, "isEnrolled": false,
"isMasquerading": false, "isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false, "isSelfPaced": false,
"isStaff": false, "isStaff": false,
"number": "DemoX", "number": "DemoX",
"org": "edX", "org": "edX",
"originalUserIsStaff": false, "originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z", "start": "2013-02-05T05:00:00Z",
"tabs": [ "tabs": Array [
{ Object {
"slug": "outline", "slug": "outline",
"title": "Course", "title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
}, },
{ Object {
"slug": "discussion", "slug": "discussion",
"title": "Discussion", "title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
}, },
{ Object {
"slug": "wiki", "slug": "wiki",
"title": "Wiki", "title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
}, },
{ Object {
"slug": "progress", "slug": "progress",
"title": "Progress", "title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
}, },
{ Object {
"slug": "instructor", "slug": "instructor",
"title": "Instructor", "title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
}, },
{ Object {
"slug": "dates", "slug": "dates",
"title": "Dates", "title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates", "url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
@@ -749,7 +601,7 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"title": "Demonstration Course", "title": "Demonstration Course",
"userTimezone": "UTC", "userTimezone": "UTC",
"username": "MockUser", "username": "MockUser",
"verifiedMode": { "verifiedMode": Object {
"accessExpirationDate": null, "accessExpirationDate": null,
"currency": "USD", "currency": "USD",
"currencySymbol": "$", "currencySymbol": "$",
@@ -759,16 +611,16 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
}, },
}, },
}, },
"progress": { "progress": Object {
"course-v1:edX+DemoX+Demo_Course": { "course-v1:edX+DemoX+Demo_Course": Object {
"accessExpiration": null, "accessExpiration": null,
"certificateData": {}, "certificateData": Object {},
"completionSummary": { "completionSummary": Object {
"completeCount": 1, "completeCount": 1,
"incompleteCount": 1, "incompleteCount": 1,
"lockedCount": 0, "lockedCount": 0,
}, },
"courseGrade": { "courseGrade": Object {
"isPassing": true, "isPassing": true,
"letterGrade": "pass", "letterGrade": "pass",
"percent": 1, "percent": 1,
@@ -779,10 +631,10 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"enrollmentMode": "audit", "enrollmentMode": "audit",
"gradesFeatureIsFullyLocked": false, "gradesFeatureIsFullyLocked": false,
"gradesFeatureIsPartiallyLocked": false, "gradesFeatureIsPartiallyLocked": false,
"gradingPolicy": { "gradingPolicy": Object {
"assignmentPolicies": [ "assignmentPolicies": Array [
{ Object {
"averageGrade": "1.0000", "averageGrade": "1.00",
"numDroppable": 1, "numDroppable": 1,
"shortLabel": "HW", "shortLabel": "HW",
"type": "Homework", "type": "Homework",
@@ -790,17 +642,17 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"weightedGrade": 1, "weightedGrade": 1,
}, },
], ],
"gradeRange": { "gradeRange": Object {
"pass": 0.75, "pass": 0.75,
}, },
}, },
"hasScheduledContent": false, "hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course", "id": "course-v1:edX+DemoX+Demo_Course",
"sectionScores": [ "sectionScores": Array [
{ Object {
"displayName": "First section", "displayName": "First section",
"subsections": [ "subsections": Array [
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345", "blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
"displayName": "First subsection", "displayName": "First subsection",
@@ -809,16 +661,16 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
"numPointsEarned": 0, "numPointsEarned": 0,
"numPointsPossible": 3, "numPointsPossible": 3,
"percentGraded": 0, "percentGraded": 0,
"problemScores": [ "problemScores": Array [
{ Object {
"earned": 0, "earned": 0,
"possible": 1, "possible": 1,
}, },
{ Object {
"earned": 0, "earned": 0,
"possible": 1, "possible": 1,
}, },
{ Object {
"earned": 0, "earned": 0,
"possible": 1, "possible": 1,
}, },
@@ -829,18 +681,18 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
}, },
], ],
}, },
{ Object {
"displayName": "Second section", "displayName": "Second section",
"subsections": [ "subsections": Array [
{ Object {
"assignmentType": "Homework", "assignmentType": "Homework",
"displayName": "Second subsection", "displayName": "Second subsection",
"hasGradedAssignment": true, "hasGradedAssignment": true,
"numPointsEarned": 1, "numPointsEarned": 1,
"numPointsPossible": 1, "numPointsPossible": 1,
"percentGraded": 1, "percentGraded": 1,
"problemScores": [ "problemScores": Array [
{ Object {
"earned": 1, "earned": 1,
"possible": 1, "possible": 1,
}, },
@@ -854,7 +706,7 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
], ],
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run", "studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
"userHasPassingGrade": false, "userHasPassingGrade": false,
"verificationData": { "verificationData": Object {
"link": null, "link": null,
"status": "none", "status": "none",
"statusDate": null, "statusDate": null,
@@ -863,75 +715,10 @@ exports[`Data layer integration tests Test fetchProgressTab Should fetch, normal
}, },
}, },
}, },
"plugins": {}, "recommendations": Object {
"recommendations": {
"recommendationsStatus": "loading", "recommendationsStatus": "loading",
}, },
"specialExams": { "tours": Object {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false, "showCoursewareTour": false,
"showExistingUserCourseHomeTour": false, "showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false, "showNewUserCourseHomeModal": false,

View File

@@ -18,7 +18,7 @@ const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) =
// Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately // Calculate the average grade for the assignment and round it. This rounding is not ideal and does not accurately
// reflect what a learner's grade would be, however, we must have parity with the current grading behavior that // reflect what a learner's grade would be, however, we must have parity with the current grading behavior that
// exists in edx-platform. // exists in edx-platform.
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4); averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(2);
weightedGrade = averageGrade * assignmentWeight; weightedGrade = averageGrade * assignmentWeight;
} }
return { averageGrade, weightedGrade }; return { averageGrade, weightedGrade };
@@ -136,7 +136,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
title: block.display_name, title: block.display_name,
resumeBlock: block.resume_block, resumeBlock: block.resume_block,
sequenceIds: block.children || [], sequenceIds: block.children || [],
hideFromTOC: block.hide_from_toc,
}; };
break; break;
@@ -153,8 +152,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
// link in the outline (even though we ignore the given url and use an internal <Link> to ourselves). // link in the outline (even though we ignore the given url and use an internal <Link> to ourselves).
showLink: !!block.lms_web_url, showLink: !!block.lms_web_url,
title: block.display_name, title: block.display_name,
hideFromTOC: block.hide_from_toc,
navigationDisabled: block.navigation_disabled,
}; };
break; break;

View File

@@ -55,29 +55,6 @@ describe('Data layer integration tests', () => {
expect(store.getState().courseHome.courseStatus).toEqual('failed'); expect(store.getState().courseHome.courseStatus).toEqual('failed');
}); });
it('should result in fetch failed if course metadata call errored', async () => {
const datesTabData = Factory.build('datesTabData');
const datesUrl = `${datesBaseUrl}/${courseId}`;
axiosMock.onGet(courseMetadataUrl).networkError();
axiosMock.onGet(datesUrl).reply(200, datesTabData);
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('should result in fetch failed if course metadata call errored', async () => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).networkError();
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(loggingService.logError).toHaveBeenCalled();
expect(store.getState().courseHome.courseStatus).toEqual('failed');
});
it('Should fetch, normalize, and save metadata', async () => { it('Should fetch, normalize, and save metadata', async () => {
const datesTabData = Factory.build('datesTabData'); const datesTabData = Factory.build('datesTabData');
@@ -101,14 +78,18 @@ describe('Data layer integration tests', () => {
}); });
it.each([401, 403, 404])( it.each([401, 403, 404])(
'should result in fetch denied if course access is denied, regardless of dates API status', 'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => { async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {}); axiosMock.onGet(`${datesBaseUrl}/${courseId}`).reply(errorStatus, {});
await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch); await executeThunk(thunks.fetchDatesTab(courseId), store.dispatch);
expect(store.getState().courseHome.courseStatus).toEqual('denied'); let expectedState = 'failed';
if (errorStatus === 401 || errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
}, },
); );
}); });
@@ -148,14 +129,18 @@ describe('Data layer integration tests', () => {
}); });
it.each([401, 403, 404])( it.each([401, 403, 404])(
'should result in fetch denied if course access is denied, regardless of outline API status', 'should result in fetch denied for expected errors and failed for all others',
async (errorStatus) => { async (errorStatus) => {
axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata); axiosMock.onGet(courseMetadataUrl).reply(200, courseHomeAccessDeniedMetadata);
axiosMock.onGet(outlineUrl).reply(errorStatus, {}); axiosMock.onGet(outlineUrl).reply(errorStatus, {});
await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch); await executeThunk(thunks.fetchOutlineTab(courseId), store.dispatch);
expect(store.getState().courseHome.courseStatus).toEqual('denied'); let expectedState = 'failed';
if (errorStatus === 403) {
expectedState = 'denied';
}
expect(store.getState().courseHome.courseStatus).toEqual(expectedState);
}, },
); );
}); });

View File

@@ -1,12 +1,10 @@
/* eslint-disable no-param-reassign */ /* eslint-disable no-param-reassign */
import { createSlice } from '@reduxjs/toolkit'; import { createSlice } from '@reduxjs/toolkit';
import { export const LOADING = 'loading';
LOADING, export const LOADED = 'loaded';
LOADED, export const FAILED = 'failed';
FAILED, export const DENIED = 'denied';
DENIED,
} from '@src/constants';
const slice = createSlice({ const slice = createSlice({
name: 'course-home', name: 'course-home',

View File

@@ -38,41 +38,28 @@ export function fetchTab(courseId, tab, getTabData, targetUserId) {
return async (dispatch) => { return async (dispatch) => {
dispatch(fetchTabRequest({ courseId })); dispatch(fetchTabRequest({ courseId }));
try { try {
const promisesToFulfill = [getCourseHomeCourseMetadata(courseId, 'outline')]; const courseHomeCourseMetadata = await getCourseHomeCourseMetadata(courseId, 'outline');
if (getTabData) { dispatch(addModel({
promisesToFulfill.push(getTabData(courseId, targetUserId)); modelType: 'courseHomeMeta',
} model: {
const [ id: courseId,
courseHomeCourseMetadataResult, ...courseHomeCourseMetadata,
tabDataResult, },
] = await Promise.allSettled(promisesToFulfill); }));
if (courseHomeCourseMetadataResult.status === 'fulfilled') { const tabDataResult = getTabData && await getTabData(courseId, targetUserId);
dispatch(addModel({ if (tabDataResult) {
modelType: 'courseHomeMeta',
model: {
id: courseId,
...courseHomeCourseMetadataResult.value,
},
}));
}
if (tabDataResult?.status === 'fulfilled') {
dispatch(addModel({ dispatch(addModel({
modelType: tab, modelType: tab,
model: { model: {
id: courseId, id: courseId,
...tabDataResult.value, ...tabDataResult,
}, },
})); }));
} }
if (courseHomeCourseMetadataResult.status === 'rejected') { // Disable the access-denied path for now - it caused a regression
throw courseHomeCourseMetadataResult.reason; if (!courseHomeCourseMetadata.courseAccess.hasAccess) {
} else if (!courseHomeCourseMetadataResult.value.courseAccess.hasAccess) {
// If the learner does not have access to the course, short cut to dispatch to a denied response regardless of
// the tabDataResult.
dispatch(fetchTabDenied({ courseId })); dispatch(fetchTabDenied({ courseId }));
} else if (tabDataResult?.status === 'rejected') { } else if (tabDataResult || !getTabData) {
throw tabDataResult.reason;
} else {
dispatch(fetchTabSuccess({ dispatch(fetchTabSuccess({
courseId, courseId,
targetUserId, targetUserId,

View File

@@ -8,7 +8,7 @@ import {
injectIntl, injectIntl,
intlShape, intlShape,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Tooltip, OverlayTrigger } from '@openedx/paragon'; import { Tooltip, OverlayTrigger } from '@edx/paragon';
import { faInfoCircle } from '@fortawesome/free-solid-svg-icons'; import { faInfoCircle } from '@fortawesome/free-solid-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -2,7 +2,7 @@ import React from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faLock } from '@fortawesome/free-solid-svg-icons'; import { faLock } from '@fortawesome/free-solid-svg-icons';
import { Badge } from '@openedx/paragon'; import { Badge } from '@edx/paragon';
import messages from '../messages'; import messages from '../messages';
import { daycmp, isLearnerAssignment } from '../utils'; import { daycmp, isLearnerAssignment } from '../utils';
@@ -38,21 +38,21 @@ function getBadgeListAndColor(date, intl, item, items) {
message: messages.today, message: messages.today,
shownForDay: isToday, shownForDay: isToday,
bg: 'bg-warning-300', bg: 'bg-warning-300',
className: 'text-dark', className: 'text-black',
}, },
{ {
message: messages.completed, message: messages.completed,
shownForDay: assignments.length && assignments.every(isComplete), shownForDay: assignments.length && assignments.every(isComplete),
shownForItem: x => isLearnerAssignment(x) && isComplete(x), shownForItem: x => isLearnerAssignment(x) && isComplete(x),
bg: 'bg-light-500', bg: 'bg-light-500',
className: 'text-dark', className: 'text-black',
}, },
{ {
message: messages.pastDue, message: messages.pastDue,
shownForDay: assignments.length && assignments.every(isPastDue), shownForDay: assignments.length && assignments.every(isPastDue),
shownForItem: x => isLearnerAssignment(x) && isPastDue(x), shownForItem: x => isLearnerAssignment(x) && isPastDue(x),
bg: 'bg-dark-200', bg: 'bg-dark-200',
className: 'text-dark', className: 'text-white',
}, },
{ {
message: messages.dueNext, message: messages.dueNext,

View File

@@ -3,7 +3,7 @@ import { useParams } from 'react-router-dom';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import HeaderSlot from '../../plugin-slots/HeaderSlot'; import { LearningHeader as Header } from '@edx/frontend-component-header';
import PageLoading from '../../generic/PageLoading'; import PageLoading from '../../generic/PageLoading';
import { unsubscribeFromCourseGoal } from '../data/api'; import { unsubscribeFromCourseGoal } from '../data/api';
@@ -38,7 +38,7 @@ const GoalUnsubscribe = ({ intl }) => {
return ( return (
<> <>
<HeaderSlot showUserDropdown={false} /> <Header showUserDropdown={false} />
<main id="main-content" className="container my-5 text-center"> <main id="main-content" className="container my-5 text-center">
{isLoading && ( {isLoading && (
<PageLoading srMessage={`${intl.formatMessage(messages.loading)}`} /> <PageLoading srMessage={`${intl.formatMessage(messages.loading)}`} />

View File

@@ -1,7 +1,7 @@
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink } from '@openedx/paragon'; import { Button, Hyperlink } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg'; import { ReactComponent as UnsubscribeIcon } from './unsubscribe.svg';

View File

@@ -9,9 +9,8 @@ const LmsHtmlFragment = ({
title, title,
...rest ...rest
}) => { }) => {
const direction = document.documentElement?.getAttribute('dir') || 'ltr';
const wholePage = ` const wholePage = `
<html dir="${direction}"> <html>
<head> <head>
<base href="${getConfig().LMS_BASE_URL}" target="_parent"> <base href="${getConfig().LMS_BASE_URL}" target="_parent">
<link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css"> <link rel="stylesheet" href="/static/${getConfig().LEGACY_THEME_NAME ? `${getConfig().LEGACY_THEME_NAME}/` : ''}css/bootstrap/lms-main.css">
@@ -30,7 +29,7 @@ const LmsHtmlFragment = ({
const iframe = useRef(null); const iframe = useRef(null);
function resetIframeHeight() { function resetIframeHeight() {
if (iframe?.current?.contentWindow?.document?.body) { if (iframe?.current?.contentWindow?.document?.body) {
iframe.current.height = iframe.current.contentWindow.document.body.parentNode.scrollHeight; iframe.current.height = iframe.current.contentWindow.document.body.scrollHeight;
} }
} }

View File

@@ -4,8 +4,7 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon'; import { Button } from '@edx/paragon';
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import { AlertList } from '../../generic/user-messages'; import { AlertList } from '../../generic/user-messages';
import CourseDates from './widgets/CourseDates'; import CourseDates from './widgets/CourseDates';
@@ -195,27 +194,19 @@ const OutlineTab = ({ intl }) => {
/> />
)} )}
<CourseTools /> <CourseTools />
<PluginSlot <UpgradeNotification
id="outline_tab_notifications_slot" offer={offer}
pluginProps={{ verifiedMode={verifiedMode}
courseId, accessExpiration={accessExpiration}
model: 'outline', contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled}
}} marketingUrl={marketingUrl}
> upsellPageName="course_home"
<UpgradeNotification userTimezone={userTimezone}
offer={offer} shouldDisplayBorder
verifiedMode={verifiedMode} timeOffsetMillis={timeOffsetMillis}
accessExpiration={accessExpiration} courseId={courseId}
contentTypeGatingEnabled={datesBannerInfo.contentTypeGatingEnabled} org={org}
marketingUrl={marketingUrl} />
upsellPageName="course_home"
userTimezone={userTimezone}
shouldDisplayBorder
timeOffsetMillis={timeOffsetMillis}
courseId={courseId}
org={org}
/>
</PluginSlot>
<CourseDates /> <CourseDates />
<CourseHandouts /> <CourseHandouts />
</div> </div>

View File

@@ -23,26 +23,8 @@ import { CERT_STATUS_TYPE } from './alerts/certificate-status-alert/CertificateS
import OutlineTab from './OutlineTab'; import OutlineTab from './OutlineTab';
import LoadedTabPage from '../../tab-page/LoadedTabPage'; import LoadedTabPage from '../../tab-page/LoadedTabPage';
const mockCoursewareSearchParams = jest.fn();
initializeMockApp(); initializeMockApp();
jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-platform/analytics');
jest.mock('../courseware-search/hooks', () => ({
...jest.requireActual('../courseware-search/hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
describe('Outline Tab', () => { describe('Outline Tab', () => {
let axiosMock; let axiosMock;
@@ -95,16 +77,9 @@ describe('Outline Tab', () => {
expiration_date: null, expiration_date: null,
}); });
// Mock courseware search params
mockSearchParams();
logUnhandledRequests(axiosMock); logUnhandledRequests(axiosMock);
}); });
afterEach(() => {
jest.clearAllMocks();
});
describe('Course Outline', () => { describe('Course Outline', () => {
it('displays link to start course', async () => { it('displays link to start course', async () => {
await fetchAndRender(); await fetchAndRender();
@@ -132,16 +107,6 @@ describe('Outline Tab', () => {
expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true'); expect(expandedSectionNode).toHaveAttribute('aria-expanded', 'true');
}); });
it('includes outline_tab_notifications_slot', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
setTabData({
course_blocks: { blocks: courseBlocks.blocks },
});
await fetchAndRender();
expect(screen.getByTestId('outline_tab_notifications_slot')).toBeInTheDocument();
});
it('handles expand/collapse all button click', async () => { it('handles expand/collapse all button click', async () => {
await fetchAndRender(); await fetchAndRender();
// Button renders as "Expand All" // Button renders as "Expand All"
@@ -292,22 +257,6 @@ describe('Outline Tab', () => {
}); });
}); });
it('ignores comments and misformatted HTML', async () => {
setTabData({
welcome_message_html: '<p class="additional-spaces-in-tag" >'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ 'Test welcome message that happens to be longer than one hundred words because of comments but displayed content is less.'
+ 'It should not be shortened.'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '<!-- Even if the welcome_message_html length is above the limit because of comments, we hope it will not be shortened. -->'
+ '</p>',
});
await fetchAndRender();
const showMoreButton = screen.queryByRole('button', { name: 'Show More' });
expect(showMoreButton).not.toBeInTheDocument();
});
it('does not display if no update available', async () => { it('does not display if no update available', async () => {
setTabData({ welcome_message_html: null }); setTabData({ welcome_message_html: null });
await fetchAndRender(); await fetchAndRender();
@@ -1295,97 +1244,5 @@ describe('Outline Tab', () => {
await waitFor(() => expect(axiosMock.history.post).toHaveLength(1)); await waitFor(() => expect(axiosMock.history.post).toHaveLength(1));
expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl); expect(axiosMock.history.post[0].url).toEqual(resendEmailUrl);
}); });
it('section should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.getByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode).toBeInTheDocument();
expect(textHiddenFromTocSectionNode.textContent).toBe('Hidden in Course Outline, accessible via link');
});
it('section should not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-icon');
const textHiddenFromTocSectionNode = screen.queryByTestId('hide-from-toc-section-text');
expect(iconHiddenFromTocSectionNode).not.toBeInTheDocument();
expect(textHiddenFromTocSectionNode).not.toBeInTheDocument();
});
it('sequence link should show hidden from toc message when hide_from_toc is true', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: true,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLinkNode = screen.getByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.getByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLinkNode).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).toBeInTheDocument();
expect(textHiddenFromTocSequenceLink.textContent).toBe('Subsections are not navigable between each other, they can only be accessed through their link.');
});
it('sequence link not show hidden from toc message when hide_from_toc is false', async () => {
const { courseBlocks } = await buildMinimalCourseBlocks(courseId, 'Title', { resumeBlock: true });
const courseBlocksIds = Object.keys(courseBlocks.blocks);
const newCourseBlocks = courseBlocksIds.reduce((blocks, blockId) => ({
...blocks,
[blockId]: {
...courseBlocks.blocks[blockId],
hide_from_toc: false,
},
}), {});
setTabData({
course_blocks: { blocks: newCourseBlocks },
});
await fetchAndRender();
const iconHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-icon');
const textHiddenFromTocSequenceLink = screen.queryByTestId('hide-from-toc-sequence-link-text');
expect(iconHiddenFromTocSequenceLink).not.toBeInTheDocument();
expect(textHiddenFromTocSequenceLink).not.toBeInTheDocument();
});
}); });
}); });

View File

@@ -1,12 +1,11 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Collapsible, IconButton, Icon } from '@openedx/paragon'; import { Collapsible, IconButton } from '@edx/paragon';
import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; import { faCheckCircle as fasCheckCircle, faMinus, faPlus } from '@fortawesome/free-solid-svg-icons';
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { DisabledVisible } from '@openedx/paragon/icons';
import SequenceLink from './SequenceLink'; import SequenceLink from './SequenceLink';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
@@ -24,7 +23,6 @@ const Section = ({
complete, complete,
sequenceIds, sequenceIds,
title, title,
hideFromTOC,
} = section; } = section;
const { const {
courseBlocks: { courseBlocks: {
@@ -44,7 +42,7 @@ const Section = ({
}, []); }, []);
const sectionTitle = ( const sectionTitle = (
<div className="d-flex row w-100 m-0"> <div className="row w-100 m-0">
<div className="col-auto p-0"> <div className="col-auto p-0">
{complete ? ( {complete ? (
<FontAwesomeIcon <FontAwesomeIcon
@@ -64,24 +62,12 @@ const Section = ({
/> />
)} )}
</div> </div>
<div className="col-7 ml-3 p-0 font-weight-bold text-dark-500"> <div className="col-10 ml-3 p-0 font-weight-bold text-dark-500">
<span className="align-middle col-6">{title}</span> <span className="align-middle">{title}</span>
<span className="sr-only"> <span className="sr-only">
, {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)} , {intl.formatMessage(complete ? messages.completedSection : messages.incompleteSection)}
</span> </span>
</div> </div>
{hideFromTOC && (
<div className="row">
{hideFromTOC && (
<span className="small d-flex align-content-end">
<Icon className="mr-2" src={DisabledVisible} data-testid="hide-from-toc-section-icon" />
<span data-testid="hide-from-toc-section-text">
{intl.formatMessage(messages.hiddenSection)}
</span>
</span>
)}
</div>
)}
</div> </div>
); );

View File

@@ -1,3 +1,4 @@
import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import classNames from 'classnames'; import classNames from 'classnames';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
@@ -11,8 +12,6 @@ import { faCheckCircle as fasCheckCircle } from '@fortawesome/free-solid-svg-ico
import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons'; import { faCheckCircle as farCheckCircle } from '@fortawesome/free-regular-svg-icons';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { Icon } from '@openedx/paragon';
import { Block } from '@openedx/paragon/icons';
import EffortEstimate from '../../shared/effort-estimate'; import EffortEstimate from '../../shared/effort-estimate';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';
import messages from './messages'; import messages from './messages';
@@ -30,7 +29,6 @@ const SequenceLink = ({
due, due,
showLink, showLink,
title, title,
hideFromTOC,
} = sequence; } = sequence;
const { const {
userTimezone, userTimezone,
@@ -95,7 +93,7 @@ const SequenceLink = ({
icon={fasCheckCircle} icon={fasCheckCircle}
fixedWidth fixedWidth
className="float-left text-success mt-1" className="float-left text-success mt-1"
aria-hidden={complete} aria-hidden="true"
title={intl.formatMessage(messages.completedAssignment)} title={intl.formatMessage(messages.completedAssignment)}
/> />
) : ( ) : (
@@ -103,7 +101,7 @@ const SequenceLink = ({
icon={farCheckCircle} icon={farCheckCircle}
fixedWidth fixedWidth
className="float-left text-gray-400 mt-1" className="float-left text-gray-400 mt-1"
aria-hidden={complete} aria-hidden="true"
title={intl.formatMessage(messages.incompleteAssignment)} title={intl.formatMessage(messages.incompleteAssignment)}
/> />
)} )}
@@ -116,16 +114,6 @@ const SequenceLink = ({
<EffortEstimate className="ml-3 align-middle" block={sequence} /> <EffortEstimate className="ml-3 align-middle" block={sequence} />
</div> </div>
</div> </div>
{hideFromTOC && (
<div className="row w-100 my-2 mx-4 pl-3">
<span className="small d-flex">
<Icon className="mr-2" src={Block} data-testid="hide-from-toc-sequence-link-icon" />
<span data-testid="hide-from-toc-sequence-link-text">
{intl.formatMessage(messages.hiddenSequenceLink)}
</span>
</span>
</div>
)}
<div className="row w-100 m-0 ml-3 pl-3"> <div className="row w-100 m-0 ml-3 pl-3">
<small className="text-body pl-2"> <small className="text-body pl-2">
{due ? dueDateMessage : noDueDateMessage} {due ? dueDateMessage : noDueDateMessage}

View File

@@ -6,7 +6,7 @@ import {
injectIntl, injectIntl,
intlShape, intlShape,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon'; import { Alert, Button } from '@edx/paragon';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -6,8 +6,8 @@ import {
FormattedRelativeTime, FormattedRelativeTime,
FormattedTime, FormattedTime,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon'; import { Alert } from '@edx/paragon';
import { Info } from '@openedx/paragon/icons'; import { Info } from '@edx/paragon/icons';
const DAY_SEC = 24 * 60 * 60; // in seconds const DAY_SEC = 24 * 60 * 60; // in seconds
const DAY_MS = DAY_SEC * 1000; // in ms const DAY_MS = DAY_SEC * 1000; // in ms

View File

@@ -3,7 +3,7 @@ import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape, FormattedMessage } from '@edx/frontend-platform/i18n';
import { getLoginRedirectUrl } from '@edx/frontend-platform/auth'; import { getLoginRedirectUrl } from '@edx/frontend-platform/auth';
import { Alert, Button, Hyperlink } from '@openedx/paragon'; import { Alert, Button, Hyperlink } from '@edx/paragon';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import { faSpinner } from '@fortawesome/free-solid-svg-icons'; import { faSpinner } from '@fortawesome/free-solid-svg-icons';

View File

@@ -1,5 +1,5 @@
import { FormattedMessage } from '@edx/frontend-platform/i18n'; import { FormattedMessage } from '@edx/frontend-platform/i18n';
import { Alert, Button } from '@openedx/paragon'; import { Alert, Button } from '@edx/paragon';
import React from 'react'; import React from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';

View File

@@ -36,16 +36,6 @@ const messages = defineMessages({
defaultMessage: 'Completed section', defaultMessage: 'Completed section',
description: 'Text used to describe the green checkmark icon in front of a section title', description: 'Text used to describe the green checkmark icon in front of a section title',
}, },
hiddenSection: {
id: 'learning.outline.hiddenSection',
defaultMessage: 'Hidden in Course Outline, accessible via link',
description: 'Label for hidden section in course outline',
},
hiddenSequenceLink: {
id: 'learning.outline.hiddenSequenceLink',
defaultMessage: 'Subsections are not navigable between each other, they can only be accessed through their link.',
description: 'Label for hidden sequence in course outline',
},
dates: { dates: {
id: 'learning.outline.dates', id: 'learning.outline.dates',
defaultMessage: 'Important dates', defaultMessage: 'Important dates',

View File

@@ -1,5 +1,5 @@
@import "~@edx/brand/paragon/variables"; @import "~@edx/brand/paragon/variables";
@import "~@openedx/paragon/scss/core/core"; @import "~@edx/paragon/scss/core/core";
@import "~@edx/brand/paragon/overrides"; @import "~@edx/brand/paragon/overrides";
.flag-button { .flag-button {

View File

@@ -3,7 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
import camelCase from 'lodash.camelcase'; import camelCase from 'lodash.camelcase';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon'; import { Button } from '@edx/paragon';
import messages from '../messages'; import messages from '../messages';
import { getProctoringInfoData } from '../../data/api'; import { getProctoringInfoData } from '../../data/api';

View File

@@ -1,5 +1,5 @@
import React from 'react'; import React from 'react';
import { Button, Card } from '@openedx/paragon'; import { Button, Card } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';

View File

@@ -2,12 +2,12 @@ import React, { useEffect, useState } from 'react';
import { useLocation } from 'react-router-dom'; import { useLocation } from 'react-router-dom';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { Form, Card, Icon } from '@openedx/paragon'; import { Form, Card, Icon } from '@edx/paragon';
import { history } from '@edx/frontend-platform'; import { history } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Email } from '@openedx/paragon/icons'; import { Email } from '@edx/paragon/icons';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import messages from '../messages'; import messages from '../messages';
import LearningGoalButton from './LearningGoalButton'; import LearningGoalButton from './LearningGoalButton';

View File

@@ -1,8 +1,8 @@
import React, { useState, useMemo } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Alert, Button, TransitionReplace } from '@openedx/paragon'; import { Alert, Button, TransitionReplace } from '@edx/paragon';
import truncate from 'truncate-html'; import truncate from 'truncate-html';
import { useDispatch } from 'react-redux'; import { useDispatch } from 'react-redux';
@@ -18,22 +18,8 @@ const WelcomeMessage = ({ courseId, intl }) => {
const [display, setDisplay] = useState(true); const [display, setDisplay] = useState(true);
// welcomeMessageHtml can contain comments or malformatted HTML which can impact the length that determines const shortWelcomeMessageHtml = truncate(welcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true });
// messageCanBeShortened. We clean it by calling truncate with a length of welcomeMessageHtml.length which const messageCanBeShortened = shortWelcomeMessageHtml.length < welcomeMessageHtml.length;
// will not result in a truncation but a formatting into 'truncate-html' canonical format.
const cleanedWelcomeMessageHtml = useMemo(
() => truncate(welcomeMessageHtml, welcomeMessageHtml.length, { keepWhitespaces: true }),
[welcomeMessageHtml],
);
const shortWelcomeMessageHtml = useMemo(
() => truncate(cleanedWelcomeMessageHtml, 100, { byWords: true, keepWhitespaces: true }),
[cleanedWelcomeMessageHtml],
);
const messageCanBeShortened = useMemo(
() => (shortWelcomeMessageHtml.length < cleanedWelcomeMessageHtml.length),
[cleanedWelcomeMessageHtml, shortWelcomeMessageHtml],
);
const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened); const [showShortMessage, setShowShortMessage] = useState(messageCanBeShortened);
const dispatch = useDispatch(); const dispatch = useDispatch();
@@ -77,7 +63,7 @@ const WelcomeMessage = ({ courseId, intl }) => {
className="inline-link" className="inline-link"
data-testid="long-welcome-message-iframe" data-testid="long-welcome-message-iframe"
key="full-html" key="full-html"
html={cleanedWelcomeMessageHtml} html={welcomeMessageHtml}
title={intl.formatMessage(messages.welcomeMessage)} title={intl.formatMessage(messages.welcomeMessage)}
/> />
)} )}

View File

@@ -3,7 +3,7 @@ import { useSelector } from 'react-redux';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Button } from '@openedx/paragon'; import { Button } from '@edx/paragon';
import { useModel } from '../../generic/model-store'; import { useModel } from '../../generic/model-store';

View File

@@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { breakpoints, useWindowSize } from '@openedx/paragon'; import { breakpoints, useWindowSize } from '@edx/paragon';
import CertificateStatus from './certificate-status/CertificateStatus'; import CertificateStatus from './certificate-status/CertificateStatus';
import CourseCompletion from './course-completion/CourseCompletion'; import CourseCompletion from './course-completion/CourseCompletion';

View File

@@ -3,7 +3,7 @@ import { Factory } from 'rosie';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth'; import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon'; import { breakpoints } from '@edx/paragon';
import MockAdapter from 'axios-mock-adapter'; import MockAdapter from 'axios-mock-adapter';
import { import {
@@ -16,26 +16,8 @@ import ProgressTab from './ProgressTab';
import LoadedTabPage from '../../tab-page/LoadedTabPage'; import LoadedTabPage from '../../tab-page/LoadedTabPage';
import messages from './grades/messages'; import messages from './grades/messages';
const mockCoursewareSearchParams = jest.fn();
initializeMockApp(); initializeMockApp();
jest.mock('@edx/frontend-platform/analytics'); jest.mock('@edx/frontend-platform/analytics');
jest.mock('../courseware-search/hooks', () => ({
...jest.requireActual('../courseware-search/hooks'),
useCoursewareSearchParams: () => mockCoursewareSearchParams,
}));
const coursewareSearch = {
query: '',
filter: '',
setQuery: jest.fn(),
setFilter: jest.fn(),
clearSearchParams: jest.fn(),
};
const mockSearchParams = ((props = coursewareSearch) => {
mockCoursewareSearchParams.mockReturnValue(props);
});
describe('Progress Tab', () => { describe('Progress Tab', () => {
let axiosMock; let axiosMock;
@@ -76,16 +58,9 @@ describe('Progress Tab', () => {
axiosMock.onGet(progressUrl).reply(200, defaultTabData); axiosMock.onGet(progressUrl).reply(200, defaultTabData);
axiosMock.onGet(masqueradeUrl).reply(200, { success: true }); axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
// Mock courseware search params
mockSearchParams();
logUnhandledRequests(axiosMock); logUnhandledRequests(axiosMock);
}); });
afterEach(() => {
jest.clearAllMocks();
});
describe('Related links', () => { describe('Related links', () => {
beforeEach(() => { beforeEach(() => {
sendTrackEvent.mockClear(); sendTrackEvent.mockClear();

View File

@@ -6,7 +6,7 @@ import {
FormattedDate, FormattedMessage, injectIntl, intlShape, FormattedDate, FormattedMessage, injectIntl, intlShape,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { Button, Card } from '@openedx/paragon'; import { Button, Card } from '@edx/paragon';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { useModel } from '../../../generic/model-store'; import { useModel } from '../../../generic/model-store';
import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils'; import { COURSE_EXIT_MODES, getCourseExitMode } from '../../../courseware/course/course-exit/utils';
@@ -19,10 +19,6 @@ const CertificateStatus = ({ intl }) => {
courseId, courseId,
} = useSelector(state => state.courseHome); } = useSelector(state => state.courseHome);
const {
entranceExamData,
} = useModel('coursewareMeta', courseId);
const { const {
isEnrolled, isEnrolled,
org, org,
@@ -46,8 +42,6 @@ const CertificateStatus = ({ intl }) => {
certificateAvailableDate, certificateAvailableDate,
} = certificateData || {}; } = certificateData || {};
const entranceExamPassed = entranceExamData?.entranceExamPassed ?? null;
const mode = getCourseExitMode( const mode = getCourseExitMode(
certificateData, certificateData,
hasScheduledContent, hasScheduledContent,
@@ -55,7 +49,6 @@ const CertificateStatus = ({ intl }) => {
userHasPassingGrade, userHasPassingGrade,
null, // CourseExitPageIsActive null, // CourseExitPageIsActive
canViewCertificate, canViewCertificate,
entranceExamPassed,
); );
const eventProperties = { const eventProperties = {
@@ -161,7 +154,7 @@ const CertificateStatus = ({ intl }) => {
certAvailabilityDate = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />; certAvailabilityDate = <FormattedDate value={certificateAvailableDate} day="numeric" month="long" year="numeric" />;
body = ( body = (
<FormattedMessage <FormattedMessage
id="progress.certificateStatus.notAvailable.endDate" id="courseCelebration.certificateBody.notAvailable.endDate"
defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are defaultMessage="This course ends on {endDate}. Final grades and any earned certificates are
scheduled to be available after {certAvailabilityDate}." scheduled to be available after {certAvailabilityDate}."
description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!" description="This shown for leaner when they are eligible for certifcate but it't not available yet, it could because leaners just finished the course quickly!"

View File

@@ -56,6 +56,11 @@ const messages = defineMessages({
defaultMessage: 'Your certificate is available!', defaultMessage: 'Your certificate is available!',
description: 'Header text when the certifcate is available', description: 'Header text when the certifcate is available',
}, },
downloadableBody: {
id: 'progress.certificateStatus.downloadableBody',
defaultMessage: 'Showcase your accomplishment on LinkedIn or your resumé today. You can download your certificate now and access it any time from your Dashboard and Profile.',
description: 'Recommending an action for learner when course certificate is available',
},
viewableButton: { viewableButton: {
id: 'progress.certificateStatus.viewableButton', id: 'progress.certificateStatus.viewableButton',
defaultMessage: 'View my certificate', defaultMessage: 'View my certificate',

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon'; import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
@@ -33,7 +33,7 @@ const CompleteDonutSegment = ({ completePercentage, intl, lockedPercentage }) =>
show={showCompletePopover} show={showCompletePopover}
placement="top" placement="top"
overlay={( overlay={(
<Popover id="complete-content-tooltip-popover" aria-hidden="true"> <Popover aria-hidden="true">
<Popover.Content> <Popover.Content>
{intl.formatMessage(messages.completeContentTooltip)} {intl.formatMessage(messages.completeContentTooltip)}
</Popover.Content> </Popover.Content>

View File

@@ -2,7 +2,7 @@ import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon'; import { OverlayTrigger, Popover } from '@edx/paragon';
import messages from './messages'; import messages from './messages';
@@ -37,7 +37,7 @@ const IncompleteDonutSegment = ({ incompletePercentage, intl }) => {
show={showIncompletePopover} show={showIncompletePopover}
placement="top" placement="top"
overlay={( overlay={(
<Popover id="incomplete-tooltip-popover" aria-hidden="true"> <Popover aria-hidden="true">
<Popover.Content> <Popover.Content>
{intl.formatMessage(messages.incompleteContentTooltip)} {intl.formatMessage(messages.incompleteContentTooltip)}
</Popover.Content> </Popover.Content>

View File

@@ -1,7 +1,7 @@
import React, { useState } from 'react'; import React, { useState } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { OverlayTrigger, Popover } from '@openedx/paragon'; import { OverlayTrigger, Popover } from '@edx/paragon';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import messages from './messages'; import messages from './messages';
@@ -36,7 +36,7 @@ const LockedDonutSegment = ({ intl, lockedPercentage }) => {
show={showLockedPopover} show={showLockedPopover}
placement="top" placement="top"
overlay={( overlay={(
<Popover id="locked-tooltip-popover" aria-hidden="true"> <Popover aria-hidden="true">
<Popover.Content> <Popover.Content>
{intl.formatMessage(messages.lockedContentTooltip)} {intl.formatMessage(messages.lockedContentTooltip)}
</Popover.Content> </Popover.Content>

View File

@@ -2,8 +2,8 @@ import React from 'react';
import { useSelector } from 'react-redux'; import { useSelector } from 'react-redux';
import { getConfig } from '@edx/frontend-platform'; import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { FormattedMessage, injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled, WatchFilled } from '@openedx/paragon/icons'; import { CheckCircle, WarningFilled, WatchFilled } from '@edx/paragon/icons';
import { Hyperlink, Icon } from '@openedx/paragon'; import { Hyperlink, Icon } from '@edx/paragon';
import { useModel } from '../../../generic/model-store'; import { useModel } from '../../../generic/model-store';
import { DashboardLink } from '../../../shared/links'; import { DashboardLink } from '../../../shared/links';

View File

@@ -3,8 +3,8 @@ import { useSelector } from 'react-redux';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { CheckCircle, WarningFilled } from '@openedx/paragon/icons'; import { CheckCircle, WarningFilled } from '@edx/paragon/icons';
import { breakpoints, Icon, useWindowSize } from '@openedx/paragon'; import { breakpoints, Icon, useWindowSize } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store'; import { useModel } from '../../../../generic/model-store';
import GradeRangeTooltip from './GradeRangeTooltip'; import GradeRangeTooltip from './GradeRangeTooltip';

View File

@@ -4,8 +4,8 @@ import { useSelector } from 'react-redux';
import { sendTrackEvent } from '@edx/frontend-platform/analytics'; import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth'; import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { injectIntl, intlShape } from '@edx/frontend-platform/i18n'; import { injectIntl, intlShape } from '@edx/frontend-platform/i18n';
import { Locked } from '@openedx/paragon/icons'; import { Locked } from '@edx/paragon/icons';
import { Button, Icon } from '@openedx/paragon'; import { Button, Icon } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store'; import { useModel } from '../../../../generic/model-store';
import messages from '../messages'; import messages from '../messages';

View File

@@ -5,7 +5,7 @@ import { useSelector } from 'react-redux';
import { import {
getLocale, injectIntl, intlShape, isRtl, getLocale, injectIntl, intlShape, isRtl,
} from '@edx/frontend-platform/i18n'; } from '@edx/frontend-platform/i18n';
import { OverlayTrigger, Popover } from '@openedx/paragon'; import { OverlayTrigger, Popover } from '@edx/paragon';
import { useModel } from '../../../../generic/model-store'; import { useModel } from '../../../../generic/model-store';

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