Compare commits
42 Commits
jkantor/pt
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
125b60b969 | ||
|
|
7318fb3ef7 | ||
|
|
7233f08d3d | ||
|
|
d6d229f1c3 | ||
|
|
47b9a436a6 | ||
|
|
e556d5b74c | ||
|
|
694d95a816 | ||
|
|
e83813da8e | ||
|
|
a54a1b8c3c | ||
|
|
d3188efbcc | ||
|
|
33f737579a | ||
|
|
870263001e | ||
|
|
af50d5a6ed | ||
|
|
7fccf7794c | ||
|
|
c760bc479b | ||
|
|
d5140a6bf0 | ||
|
|
9bf5d01c41 | ||
|
|
f3334085d7 | ||
|
|
4840fff44b | ||
|
|
579bd0365b | ||
|
|
2b4a9661a5 | ||
|
|
a6e4e28e58 | ||
|
|
e6f7588ccd | ||
|
|
db29e314c3 | ||
|
|
e9121f9261 | ||
|
|
9cbc2276d6 | ||
|
|
4c8aa7c80c | ||
|
|
68926334a1 | ||
|
|
56a73eee15 | ||
|
|
bf95916063 | ||
|
|
48270c35dd | ||
|
|
33d7d669d9 | ||
|
|
a75c89cd14 | ||
|
|
06902d8ae8 | ||
|
|
e4134641e6 | ||
|
|
77fcc83efd | ||
|
|
b0505352be | ||
|
|
ddbc2124ef | ||
|
|
462e75f6a6 | ||
|
|
bc4c8c2dec | ||
|
|
ecd5164806 | ||
|
|
44d952bef7 |
1
.env
@@ -53,3 +53,4 @@ OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
FEATURE_ENABLE_CHAT_V2_ENDPOINT=''
|
||||
|
||||
@@ -55,3 +55,4 @@ OPTIMIZELY_FULL_STACK_SDK_KEY=''
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
|
||||
# Fallback in local style files
|
||||
PARAGON_THEME_URLS={}
|
||||
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'
|
||||
|
||||
10
.github/workflows/validate.yml
vendored
@@ -10,8 +10,8 @@ jobs:
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-node@v4
|
||||
- uses: actions/checkout@v5
|
||||
- uses: actions/setup-node@v6
|
||||
with:
|
||||
node-version-file: '.nvmrc'
|
||||
- run: make validate.ci
|
||||
@@ -24,11 +24,11 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
needs: tests
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
- name: Download code coverage results
|
||||
uses: actions/download-artifact@v4
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: code-coverage-report
|
||||
pattern: code-coverage-report
|
||||
- name: Upload coverage
|
||||
uses: codecov/codecov-action@v5
|
||||
with:
|
||||
|
||||
5
Makefile
@@ -40,10 +40,9 @@ pull_translations:
|
||||
translations/frontend-component-header/src/i18n/messages:frontend-component-header \
|
||||
translations/frontend-component-footer/src/i18n/messages:frontend-component-footer \
|
||||
translations/frontend-lib-special-exams/src/i18n/messages:frontend-lib-special-exams \
|
||||
translations/frontend-app-learning/src/i18n/messages:frontend-app-learning \
|
||||
$(ATLAS_EXTRA_SOURCES)
|
||||
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 $(ATLAS_EXTRA_INTL_IMPORTS)
|
||||
$(intl_imports) frontend-platform paragon frontend-component-header frontend-component-footer frontend-lib-special-exams frontend-app-learning
|
||||
|
||||
|
||||
# This target is used by Travis.
|
||||
|
||||
1227
package-lock.json
generated
@@ -12,17 +12,16 @@
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.23.1",
|
||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/frontend-platform": "^8.4.0",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@openedx/frontend-build": "^14.5.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@popperjs/core": "2.11.8",
|
||||
@@ -52,7 +51,6 @@
|
||||
"truncate-html": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@pact-foundation/pact": "^13.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
@@ -70,6 +68,7 @@
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.2.tgz",
|
||||
"integrity": "sha512-baYZExFpsdkBNuvGKTKWCwKH57HRZLVtycZS05WTQNVOiXVSeAki3nU35zlRbToeMW8aHlJfyS+1C4BOv27q0A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@ampproject/remapping": {
|
||||
@@ -124,14 +123,14 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/code-frame": {
|
||||
"version": "7.26.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
|
||||
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
|
||||
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.25.9",
|
||||
"@babel/helper-validator-identifier": "^7.27.1",
|
||||
"js-tokens": "^4.0.0",
|
||||
"picocolors": "^1.0.0"
|
||||
"picocolors": "^1.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -151,6 +150,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
|
||||
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.26.2",
|
||||
@@ -413,9 +413,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@babel/helper-validator-identifier": {
|
||||
"version": "7.25.9",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
|
||||
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
|
||||
"version": "7.27.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
|
||||
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6.9.0"
|
||||
@@ -2139,6 +2139,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
},
|
||||
@@ -2161,6 +2162,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": "^14 || ^16 || >=18"
|
||||
}
|
||||
@@ -2208,7 +2210,8 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.5.0.tgz",
|
||||
"integrity": "sha512-d2ggwi5j4DOBJOwhWZxBWQSDR0DhT4ke/1PbzRauICdFkuOyax+PsFjK8GUh443K2OaQpy9PGfiCzZ1Yg37AUA==",
|
||||
"license": "AGPL-3.0"
|
||||
"license": "AGPL-3.0",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/@edx/eslint-config": {
|
||||
"version": "4.3.0",
|
||||
@@ -2228,16 +2231,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer": {
|
||||
"version": "14.7.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.7.1.tgz",
|
||||
"integrity": "sha512-LsT3b1xtZdPeQmIlej+voN3RvGOjl0bUq2JEEtELESRr0F6bNVySmKFzrPwD4wActlMaeyQrat53ZZeK+NQNrw==",
|
||||
"version": "14.9.2",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.9.2.tgz",
|
||||
"integrity": "sha512-koYtfZK9flTO3ExAmaP0HDlxbV9XX8hbRE/8WNtMJh+X1B8xppT3Ft8vhGDsw6dEBo9ojndmU9805G/a8/8o3g==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.2",
|
||||
"@fortawesome/react-fontawesome": "0.2.6",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
@@ -2289,34 +2292,31 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
|
||||
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
|
||||
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header": {
|
||||
"version": "6.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.4.0.tgz",
|
||||
"integrity": "sha512-RNV3XRXhhN9QlhAoP26CjzoRIPlLSYDp3PZCnK6g6kIHgxC9dCpu2PTZdxV2AVChqVuxtZK5zLbk9yeAtf4U/A==",
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-8.0.0.tgz",
|
||||
"integrity": "sha512-AD6ImSI2APSKBOA1a3P4/Uy6YlpEYlSePpKbNNr2YjmY5nB5yWcNjE38sBBIBHLbC2/b+AwgGrv+m1bxej5xTQ==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "6.6.0",
|
||||
"@fortawesome/free-brands-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-regular-svg-icons": "6.6.0",
|
||||
"@fortawesome/free-solid-svg-icons": "6.6.0",
|
||||
"@fortawesome/fontawesome-svg-core": "6.7.2",
|
||||
"@fortawesome/free-brands-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-regular-svg-icons": "6.7.2",
|
||||
"@fortawesome/free-solid-svg-icons": "6.7.2",
|
||||
"@fortawesome/react-fontawesome": "^0.2.0",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"axios-mock-adapter": "1.22.0",
|
||||
"babel-polyfill": "6.26.0",
|
||||
"classnames": "^2.5.1",
|
||||
"jest-environment-jsdom": "^29.7.0",
|
||||
"react-responsive": "8.2.0",
|
||||
"react-transition-group": "4.4.5"
|
||||
},
|
||||
@@ -2329,93 +2329,59 @@
|
||||
"react-router-dom": "^6.14.2"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-common-types": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-common-types/-/fontawesome-common-types-6.6.0.tgz",
|
||||
"integrity": "sha512-xyX0X9mc0kyz9plIyryrRbl7ngsA9jz77mCZJsUkLl+ZKs0KWObgaEBoSgQiYWAsSmjz/yjl0F++Got0Mdp4Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/fontawesome-svg-core": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.6.0.tgz",
|
||||
"integrity": "sha512-KHwPkCk6oRT4HADE7smhfsKudt9N/9lm6EJ5BVg0tD1yPA5hht837fB87F8pn15D8JfTqQOjhKTktwmLMiD7Kg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-brands-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-1MPD8lMNW/earme4OQi1IFHtmHUwAKgghXlNwWi9GO7QkTfD+IIaYpIai4m2YJEzqfEji3jFHX1DZI5pbY/biQ==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-brands-svg-icons/-/free-brands-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-zu0evbcRTgjKfrr77/2XX+bU+kuGfjm0LbajJHVIgBWNIDzrhpRxiCPNT8DW5AdmSsq7Mcf9D1bH0aSeSUSM+Q==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-Yv9hDzL4aI73BEwSEh20clrY8q/uLxawaQ98lekBx6t9dQKDHcDzzV1p2YtBGTtolYtNqcWdniOnhzB+JPnQEQ==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-regular-svg-icons/-/free-regular-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-7Z/ur0gvCMW8G93dXIQOkQqHo2M5HLhYrRVC0//fakJXxcF1VmMPsxnG6Ee8qEylA8b8Q3peQXWMNZ62lYF28g==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
|
||||
"version": "6.6.0",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.6.0.tgz",
|
||||
"integrity": "sha512-IYv/2skhEDFc2WGUcqvFJkeK39Q+HyPf5GHUrT/l2pKbtgEIv1al1TKd6qStR5OIwQdN1GZP54ci3y4mroJWjA==",
|
||||
"version": "6.7.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/free-solid-svg-icons/-/free-solid-svg-icons-6.7.2.tgz",
|
||||
"integrity": "sha512-GsBrnOzU8uj0LECDfD5zomZJIjrPhIlWU82AHwa2s40FKH+kcxQaBvBo3Z4TxyZHIyX8XTDxsyA33/Vx9eFuQA==",
|
||||
"license": "(CC-BY-4.0 AND MIT)",
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.6.0"
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.2.tgz",
|
||||
"integrity": "sha512-EnkrprPNqI6SXJl//m29hpaNzOp1bruISWaOiRtkMi/xSvHJlzc2j2JAYS7egxt/EbjSNV/k6Xy0AQI6vB2+1g==",
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
|
||||
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"prop-types": "^15.8.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
|
||||
"react": ">=16.3"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-component-header/node_modules/axios-mock-adapter": {
|
||||
"version": "1.22.0",
|
||||
"resolved": "https://registry.npmjs.org/axios-mock-adapter/-/axios-mock-adapter-1.22.0.tgz",
|
||||
"integrity": "sha512-dmI0KbkyAhntUR05YY96qg2H6gg0XMl2+qTW0xmYg6Up+BFBAJYRLROMXRdDEL06/Wqwa0TJThAYvFtSFdRCZw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"is-buffer": "^2.0.5"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"axios": ">= 0.17.0"
|
||||
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
|
||||
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-lib-learning-assistant": {
|
||||
"version": "2.24.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.24.0.tgz",
|
||||
"integrity": "sha512-+RwmKbYxsJ6Ct9scBX3jnxSUuoiW5ed1vbCz9PQiQ8fobuiMM3fokLynIreB5ZVYWvrjSa5OaMwBq1bUXsprZw==",
|
||||
"version": "2.23.1",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.23.1.tgz",
|
||||
"integrity": "sha512-0rDHlE3tlADWOcqKaVIKkMK2YGonbRaYJfmBSgH+Sn6+BFg2e541fn7NC9e5rIaiV1BnMREF7dxyRa/IEYLZLA==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
|
||||
@@ -2481,6 +2447,7 @@
|
||||
"integrity": "sha512-0KNN0nc5eIzaJxlv43QcDmTkDY1CqeN6J7OCGSs+fwGPdtv0yOQqRjieopBCmw+yd7uD3N2HeNL3Zm5isDleLg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "^0.2.34"
|
||||
},
|
||||
@@ -2538,10 +2505,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/frontend-platform": {
|
||||
"version": "8.3.7",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.3.7.tgz",
|
||||
"integrity": "sha512-ya5ObMvtJlfQmoeL36OtzjFBh0hzJgXN/R2ppyIJ+IbCtY2BCfv5NqvmKD7CplwnSGJTBugpv5hQHeGmi+v97w==",
|
||||
"version": "8.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.4.0.tgz",
|
||||
"integrity": "sha512-toWMU7qVx56f5bLk6/Sl5WWqlKtGp602qDs22JYp5r2VBp5F/nzcrpXXWC925/kH0TP5hI2OMolmLq6n2N8a4Q==",
|
||||
"license": "AGPL-3.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@cospired/i18n-iso-languages": "4.2.0",
|
||||
"@formatjs/intl-pluralrules": "4.3.3",
|
||||
@@ -2576,6 +2544,12 @@
|
||||
"react-redux": "^7.1.1 || ^8.1.1",
|
||||
"react-router-dom": "^6.0.0",
|
||||
"redux": "^4.0.4"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"@openedx/frontend-build": {
|
||||
"optional": true,
|
||||
"reason": "This package is only a peer dependency to ensure using a minimum compatible version that provides env.config and PARAGON_THEME support. It is not needed at runtime, and may be omitted with `--omit=optional`."
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/new-relic-source-map-webpack-plugin": {
|
||||
@@ -2596,54 +2570,6 @@
|
||||
"atlas": "atlas"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/react-unit-test-utils": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/react-unit-test-utils/-/react-unit-test-utils-4.0.0.tgz",
|
||||
"integrity": "sha512-QlVYhYD9L2bzx1eAtf8BbCJr00ek9rrHrG+/pW2bVSt+t0uvKHQpX1CNdMrDePv18DsMeC7IOB00t8ZIn4mi7w==",
|
||||
"license": "AGPL-3.0",
|
||||
"dependencies": {
|
||||
"@edx/browserslist-config": "^1.1.1",
|
||||
"@reduxjs/toolkit": "^1.5.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"classnames": "^2.2.6",
|
||||
"core-js": "3.6.5",
|
||||
"lodash": "^4.17.21",
|
||||
"react-dev-utils": "^12.0.1",
|
||||
"react-test-renderer": "^18.3.1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@openedx/frontend-build": "^14.3.0",
|
||||
"@openedx/paragon": "^22.0.0 || ^23.0.0",
|
||||
"react": "^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/reactifex": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/reactifex/-/reactifex-2.2.0.tgz",
|
||||
"integrity": "sha512-vyGDtx3BwCr6Gjbm4y6gJ8Bzc2TOSNBlBa2hMerz59HoXaot14MihxxiDU+JDNybGLLcKDBiK511bOi/77i1lw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"axios": "^0.21.1",
|
||||
"yargs": "^17.1.1"
|
||||
},
|
||||
"bin": {
|
||||
"edx_reactifex": "main.js"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/reactifex/node_modules/axios": {
|
||||
"version": "0.21.4",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
|
||||
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@edx/typescript-config": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@edx/typescript-config/-/typescript-config-1.1.0.tgz",
|
||||
@@ -2665,9 +2591,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@emnapi/runtime": {
|
||||
"version": "1.4.3",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz",
|
||||
"integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==",
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
|
||||
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
@@ -3155,6 +3081,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
|
||||
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@fortawesome/fontawesome-common-types": "6.7.2"
|
||||
},
|
||||
@@ -3306,6 +3233,424 @@
|
||||
"deprecated": "Use @eslint/object-schema instead",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-arm64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
|
||||
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-darwin-x64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
|
||||
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-arm64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
|
||||
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-darwin-x64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
|
||||
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
|
||||
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-arm64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
|
||||
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-ppc64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
|
||||
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-s390x": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
|
||||
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linux-x64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
|
||||
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
|
||||
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
|
||||
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
|
||||
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-arm64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
|
||||
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-ppc64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
|
||||
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-s390x": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
|
||||
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linux-x64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
|
||||
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linux-x64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-arm64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
|
||||
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-linuxmusl-x64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
|
||||
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-wasm32": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
|
||||
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
|
||||
"cpu": [
|
||||
"wasm32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"@emnapi/runtime": "^1.4.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-arm64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
|
||||
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-ia32": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
|
||||
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@img/sharp-win32-x64": {
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
|
||||
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "Apache-2.0 AND LGPL-3.0-or-later",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
}
|
||||
},
|
||||
"node_modules/@isaacs/cliui": {
|
||||
"version": "8.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
|
||||
@@ -3609,6 +3954,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
|
||||
"integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bcoe/v8-coverage": "^0.2.3",
|
||||
"@jest/console": "^29.7.0",
|
||||
@@ -3708,6 +4054,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
|
||||
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.11.6",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -3734,6 +4081,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
|
||||
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/schemas": "^29.6.3",
|
||||
"@types/istanbul-lib-coverage": "^2.0.0",
|
||||
@@ -4010,10 +4358,11 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-build": {
|
||||
"version": "14.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.5.0.tgz",
|
||||
"integrity": "sha512-HY0PdXvXBxrvJHj8HsRA+VNCHDePENFhqOIvbSz9Ke7HDwHpfDjg2CeKk41aU/8iTyj3eESfPwKQr5fTE0A3Ww==",
|
||||
"version": "14.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.2.tgz",
|
||||
"integrity": "sha512-Iu4/GPq90Xr/MSWnonn2qX8VDhI89HN7KOYBZ0/sxmAQgvXXNc7OYNC7kumvzbYzKueJQTyZoUYS7UjKB/n1WA==",
|
||||
"license": "AGPL-3.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/cli": "7.24.8",
|
||||
"@babel/core": "7.24.9",
|
||||
@@ -4058,7 +4407,7 @@
|
||||
"file-loader": "6.2.0",
|
||||
"html-webpack-plugin": "5.6.3",
|
||||
"identity-obj-proxy": "3.0.0",
|
||||
"image-minimizer-webpack-plugin": "3.8.3",
|
||||
"image-minimizer-webpack-plugin": "4.1.4",
|
||||
"jest": "29.7.0",
|
||||
"jest-environment-jsdom": "29.7.0",
|
||||
"mini-css-extract-plugin": "1.6.2",
|
||||
@@ -4066,13 +4415,13 @@
|
||||
"postcss": "8.4.49",
|
||||
"postcss-custom-media": "10.0.8",
|
||||
"postcss-loader": "7.3.4",
|
||||
"postcss-rtlcss": "5.1.2",
|
||||
"postcss-rtlcss": "5.7.1",
|
||||
"react-dev-utils": "12.0.1",
|
||||
"react-refresh": "0.16.0",
|
||||
"resolve-url-loader": "5.0.0",
|
||||
"sass": "1.85.1",
|
||||
"sass-loader": "13.3.3",
|
||||
"sharp": "0.32.6",
|
||||
"sharp": "0.34.3",
|
||||
"source-map-loader": "4.0.2",
|
||||
"style-loader": "3.3.4",
|
||||
"ts-jest": "29.1.4",
|
||||
@@ -4098,6 +4447,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
|
||||
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@ampproject/remapping": "^2.2.0",
|
||||
"@babel/code-frame": "^7.24.7",
|
||||
@@ -4169,6 +4519,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.7",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -4201,9 +4552,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-build/node_modules/postcss-loader/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -4230,6 +4581,7 @@
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
|
||||
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"chokidar": "^4.0.0",
|
||||
"immutable": "^5.0.2",
|
||||
@@ -4350,9 +4702,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@openedx/frontend-build/node_modules/ts-jest/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -4443,6 +4795,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.4.5.tgz",
|
||||
"integrity": "sha512-baBTZDO6hdCjI+Jj3oSQaz5GkFTR2TEs94pPMOls7bc/Fsv4zQIN8xDPo4NzAVo/2+3eSuEzUz3xzBeb+94rtw==",
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"workspaces": [
|
||||
"example",
|
||||
"component-generator",
|
||||
@@ -5163,6 +5516,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
|
||||
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/popperjs"
|
||||
@@ -5173,6 +5527,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz",
|
||||
"integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"immer": "^9.0.21",
|
||||
"redux": "^4.2.1",
|
||||
@@ -5444,6 +5799,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
|
||||
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/core": "^7.21.3",
|
||||
"@svgr/babel-preset": "8.1.0",
|
||||
@@ -5546,7 +5902,9 @@
|
||||
"version": "10.4.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz",
|
||||
"integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.10.4",
|
||||
"@babel/runtime": "^7.12.5",
|
||||
@@ -5565,6 +5923,7 @@
|
||||
"version": "6.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.6.3.tgz",
|
||||
"integrity": "sha512-IteBhl4XqYNkM54f4ejhLRJiZNqcSCoXUOG2CPK7qbD322KjQozM4kHQOfkG2oln9b9HTYqs+Sae8vBATubxxA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@adobe/css-tools": "^4.4.0",
|
||||
@@ -5585,6 +5944,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz",
|
||||
"integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.1.0",
|
||||
@@ -5598,12 +5958,14 @@
|
||||
"version": "0.6.3",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz",
|
||||
"integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@testing-library/react": {
|
||||
"version": "16.3.0",
|
||||
"resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz",
|
||||
"integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.12.5"
|
||||
@@ -5699,6 +6061,7 @@
|
||||
"version": "5.0.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
|
||||
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/babel__core": {
|
||||
@@ -6110,6 +6473,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
|
||||
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"csstype": "^3.0.2"
|
||||
@@ -6246,6 +6610,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
|
||||
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
@@ -6292,6 +6657,7 @@
|
||||
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
|
||||
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
|
||||
"license": "BSD-2-Clause",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@typescript-eslint/scope-manager": "5.62.0",
|
||||
"@typescript-eslint/types": "5.62.0",
|
||||
@@ -6958,6 +7324,7 @@
|
||||
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
|
||||
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"bin": {
|
||||
"acorn": "bin/acorn"
|
||||
},
|
||||
@@ -7033,6 +7400,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
|
||||
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.1",
|
||||
"fast-json-stable-stringify": "^2.0.0",
|
||||
@@ -7520,6 +7888,7 @@
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
"form-data": "^4.0.0",
|
||||
@@ -7569,17 +7938,12 @@
|
||||
"node": ">= 0.4"
|
||||
}
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.7",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
|
||||
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/babel-jest": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
|
||||
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@jest/transform": "^29.7.0",
|
||||
"@types/babel__core": "^7.1.14",
|
||||
@@ -7835,78 +8199,6 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/bare-events": {
|
||||
"version": "2.5.4",
|
||||
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
|
||||
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/bare-fs": {
|
||||
"version": "4.1.2",
|
||||
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
|
||||
"integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-events": "^2.5.4",
|
||||
"bare-path": "^3.0.0",
|
||||
"bare-stream": "^2.6.4"
|
||||
},
|
||||
"engines": {
|
||||
"bare": ">=1.16.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/bare-os": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
|
||||
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"engines": {
|
||||
"bare": ">=1.14.0"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
|
||||
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"bare-os": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/bare-stream": {
|
||||
"version": "2.6.5",
|
||||
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
|
||||
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
|
||||
"license": "Apache-2.0",
|
||||
"optional": true,
|
||||
"dependencies": {
|
||||
"streamx": "^2.21.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bare-buffer": "*",
|
||||
"bare-events": "*"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bare-buffer": {
|
||||
"optional": true
|
||||
},
|
||||
"bare-events": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/base64-js": {
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
|
||||
@@ -8105,6 +8397,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001688",
|
||||
"electron-to-chromium": "^1.5.73",
|
||||
@@ -8322,9 +8615,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001721",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001721.tgz",
|
||||
"integrity": "sha512-cOuvmUVtKrtEaoKiO0rSc29jcjwMwX5tOHDy4MgVFEWiUXj4uBMJkwI8MDySkgXidpMiHUcviogAvFi4pA2hDQ==",
|
||||
"version": "1.0.30001751",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
|
||||
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "opencollective",
|
||||
@@ -8565,12 +8858,6 @@
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/chownr": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz",
|
||||
"integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/chroma-js": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.6.0.tgz",
|
||||
@@ -9088,18 +9375,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js": {
|
||||
"version": "3.6.5",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.5.tgz",
|
||||
"integrity": "sha512-vZVEEwZoIsI+vPEuoF9Iqf5H7/M3eeQqWlQnYa8FSKKePuYTf5MWnxb5SDAzCa60b3JBRS5g9b+Dq7b1y/RCrA==",
|
||||
"deprecated": "core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js.",
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.41.0",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.41.0.tgz",
|
||||
@@ -9330,6 +9605,7 @@
|
||||
"version": "1.5.1",
|
||||
"resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz",
|
||||
"integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/cssesc": {
|
||||
@@ -9615,21 +9891,6 @@
|
||||
"node": ">=0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/decompress-response": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz",
|
||||
"integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"mimic-response": "^3.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/dedent": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz",
|
||||
@@ -9644,15 +9905,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/deep-extend": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz",
|
||||
"integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/deep-is": {
|
||||
"version": "0.1.4",
|
||||
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
|
||||
@@ -9967,6 +10219,7 @@
|
||||
"version": "0.5.16",
|
||||
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
|
||||
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/dom-converter": {
|
||||
@@ -10237,6 +10490,7 @@
|
||||
"version": "1.4.4",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
|
||||
"integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"once": "^1.4.0"
|
||||
@@ -10543,6 +10797,7 @@
|
||||
"integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==",
|
||||
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@eslint-community/eslint-utils": "^4.2.0",
|
||||
"@eslint-community/regexpp": "^4.4.0",
|
||||
@@ -10599,6 +10854,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz",
|
||||
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"object.assign": "^4.1.2",
|
||||
@@ -10639,6 +10895,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz",
|
||||
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"eslint-config-airbnb-base": "^15.0.0"
|
||||
},
|
||||
@@ -11055,6 +11312,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
|
||||
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@rtsao/scc": "^1.1.0",
|
||||
"array-includes": "^3.1.8",
|
||||
@@ -11109,6 +11367,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
|
||||
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.20.7",
|
||||
"aria-query": "^5.1.3",
|
||||
@@ -11145,6 +11404,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
|
||||
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"array-includes": "^3.1.6",
|
||||
"array.prototype.flatmap": "^1.3.1",
|
||||
@@ -11175,6 +11435,7 @@
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz",
|
||||
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -11522,15 +11783,6 @@
|
||||
"node": ">= 0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/expand-template": {
|
||||
"version": "2.0.3",
|
||||
"resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz",
|
||||
"integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==",
|
||||
"license": "(MIT OR WTFPL)",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/expect": {
|
||||
"version": "29.7.0",
|
||||
"resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz",
|
||||
@@ -11653,12 +11905,6 @@
|
||||
"integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-fifo": {
|
||||
"version": "1.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz",
|
||||
"integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fast-glob": {
|
||||
"version": "3.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz",
|
||||
@@ -12265,12 +12511,6 @@
|
||||
"node": ">= 0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/fs-constants": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz",
|
||||
"integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/fs-extra": {
|
||||
"version": "9.1.0",
|
||||
"resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-9.1.0.tgz",
|
||||
@@ -12470,12 +12710,6 @@
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/github-from-package": {
|
||||
"version": "0.0.0",
|
||||
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
|
||||
"integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/glob": {
|
||||
"version": "7.2.3",
|
||||
"resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
|
||||
@@ -12628,6 +12862,7 @@
|
||||
"integrity": "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"iterall": "^1.2.2"
|
||||
},
|
||||
@@ -13279,16 +13514,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/image-minimizer-webpack-plugin": {
|
||||
"version": "3.8.3",
|
||||
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-3.8.3.tgz",
|
||||
"integrity": "sha512-Ex0cjNJc2FUSuwN7WHNyxkIZINP0M9lrN+uWJznMcsehiM5Z7ELwk+SEkSGEookK1GUd2wf+09jy1PEH5a5XmQ==",
|
||||
"version": "4.1.4",
|
||||
"resolved": "https://registry.npmjs.org/image-minimizer-webpack-plugin/-/image-minimizer-webpack-plugin-4.1.4.tgz",
|
||||
"integrity": "sha512-A2DLYuCyu7icbGdv8OMGFQKPXvsztWAueBkT3yQ7KVW1YGnAJKtgLYELkN7/aUday05DzEdKRaLE5Bnh/9S2UQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"schema-utils": "^4.2.0",
|
||||
"serialize-javascript": "^6.0.1"
|
||||
"serialize-javascript": "^6.0.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 12.13.0"
|
||||
"node": ">= 18.12.0"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -13415,6 +13650,7 @@
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -14930,6 +15166,7 @@
|
||||
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
|
||||
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/code-frame": "^7.12.13",
|
||||
"@jest/types": "^29.6.3",
|
||||
@@ -15377,9 +15614,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/js-toml": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.1.tgz",
|
||||
"integrity": "sha512-rHd/IolpFm2V5BmHCEY8CckHs8NDsYZZ64H5RNgA6Opsr9vX4QyTiQPplgtqg7b3ztqYShZC38nl6CUg7QuhXg==",
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.2.tgz",
|
||||
"integrity": "sha512-/7IQ//bzn2a/5IDazPUNzlW7bsjxS51cxciYZDR+Z+3Le60yzT0YfI8KOWqTtBcZkXXVklhWd2OuGd8ZksB0wQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chevrotain": "^11.0.3",
|
||||
@@ -15993,6 +16230,7 @@
|
||||
"version": "1.5.0",
|
||||
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
|
||||
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"lz-string": "bin/bin.js"
|
||||
@@ -16731,22 +16969,11 @@
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/mimic-response": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz",
|
||||
"integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
@@ -16827,12 +17054,6 @@
|
||||
"node": ">=16 || 14 >=14.17"
|
||||
}
|
||||
},
|
||||
"node_modules/mkdirp-classic": {
|
||||
"version": "0.5.3",
|
||||
"resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz",
|
||||
"integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/mri": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
|
||||
@@ -16902,12 +17123,6 @@
|
||||
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/napi-build-utils": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz",
|
||||
"integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/napi-postinstall": {
|
||||
"version": "0.1.6",
|
||||
"resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.1.6.tgz",
|
||||
@@ -16960,30 +17175,6 @@
|
||||
"tslib": "^2.0.3"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi": {
|
||||
"version": "3.74.0",
|
||||
"resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.74.0.tgz",
|
||||
"integrity": "sha512-c5XK0MjkGBrQPGYG24GBADZud0NCbznxNx0ZkS+ebUTrmV1qTDxPxSL8zEAPURXSbLRWVexxmP4986BziahL5w==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"semver": "^7.3.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-abi/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/node-addon-api": {
|
||||
"version": "7.1.1",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz",
|
||||
@@ -18122,6 +18313,7 @@
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"nanoid": "^3.3.8",
|
||||
"picocolors": "^1.1.1",
|
||||
@@ -18817,12 +19009,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/postcss-rtlcss": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.1.2.tgz",
|
||||
"integrity": "sha512-cmcgRoO1wL7IJyVHw0RneWI/5Oe75NLC2NLlQLsNI7hcui+yRcW4RrILfQa4FqKQRLTU4r5eF0YPi1qZpMzQpA==",
|
||||
"version": "5.7.1",
|
||||
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.7.1.tgz",
|
||||
"integrity": "sha512-zE68CuARv5StOG/UQLa0W1Y/raUTzgJlfjtas43yh3/G1BFmoPEaHxPRHgeowXRFFhW33FehrNgsljxRLmPVWw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"rtlcss": "4.1.1"
|
||||
"rtlcss": "4.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
@@ -18881,69 +19073,6 @@
|
||||
"integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/prebuild-install": {
|
||||
"version": "7.1.3",
|
||||
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
|
||||
"integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"detect-libc": "^2.0.0",
|
||||
"expand-template": "^2.0.3",
|
||||
"github-from-package": "0.0.0",
|
||||
"minimist": "^1.2.3",
|
||||
"mkdirp-classic": "^0.5.3",
|
||||
"napi-build-utils": "^2.0.0",
|
||||
"node-abi": "^3.3.0",
|
||||
"pump": "^3.0.0",
|
||||
"rc": "^1.2.7",
|
||||
"simple-get": "^4.0.0",
|
||||
"tar-fs": "^2.0.0",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
},
|
||||
"bin": {
|
||||
"prebuild-install": "bin.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/detect-libc": {
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
|
||||
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-fs": {
|
||||
"version": "2.1.3",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz",
|
||||
"integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"chownr": "^1.1.1",
|
||||
"mkdirp-classic": "^0.5.2",
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^2.1.4"
|
||||
}
|
||||
},
|
||||
"node_modules/prebuild-install/node_modules/tar-stream": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz",
|
||||
"integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"bl": "^4.0.3",
|
||||
"end-of-stream": "^1.4.1",
|
||||
"fs-constants": "^1.0.0",
|
||||
"inherits": "^2.0.3",
|
||||
"readable-stream": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/prelude-ls": {
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz",
|
||||
@@ -18982,6 +19111,7 @@
|
||||
"version": "27.5.1",
|
||||
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
|
||||
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1",
|
||||
@@ -18996,6 +19126,7 @@
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
|
||||
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -19008,6 +19139,7 @@
|
||||
"version": "17.0.2",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
|
||||
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/process": {
|
||||
@@ -19050,6 +19182,7 @@
|
||||
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
|
||||
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.4.0",
|
||||
"object-assign": "^4.1.1",
|
||||
@@ -19120,6 +19253,7 @@
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz",
|
||||
"integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"end-of-stream": "^1.1.0",
|
||||
@@ -19340,35 +19474,12 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/rc": {
|
||||
"version": "1.2.8",
|
||||
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
|
||||
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
|
||||
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
|
||||
"dependencies": {
|
||||
"deep-extend": "^0.6.0",
|
||||
"ini": "~1.3.0",
|
||||
"minimist": "^1.2.0",
|
||||
"strip-json-comments": "~2.0.1"
|
||||
},
|
||||
"bin": {
|
||||
"rc": "cli.js"
|
||||
}
|
||||
},
|
||||
"node_modules/rc/node_modules/strip-json-comments": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
|
||||
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
|
||||
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0"
|
||||
},
|
||||
@@ -19522,6 +19633,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
|
||||
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"loose-envify": "^1.1.0",
|
||||
"scheduler": "^0.23.2"
|
||||
@@ -19836,6 +19948,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
|
||||
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.15.4",
|
||||
"@types/react-redux": "^7.1.20",
|
||||
@@ -19867,6 +19980,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz",
|
||||
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
@@ -19941,6 +20055,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz",
|
||||
"integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.8.0"
|
||||
},
|
||||
@@ -19956,6 +20071,7 @@
|
||||
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz",
|
||||
"integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@remix-run/router": "1.8.0",
|
||||
"react-router": "6.15.0"
|
||||
@@ -19968,19 +20084,6 @@
|
||||
"react-dom": ">=16.8"
|
||||
}
|
||||
},
|
||||
"node_modules/react-shallow-renderer": {
|
||||
"version": "16.15.0",
|
||||
"resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz",
|
||||
"integrity": "sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"object-assign": "^4.1.1",
|
||||
"react-is": "^16.12.0 || ^17.0.0 || ^18.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^16.0.0 || ^17.0.0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-share": {
|
||||
"version": "4.4.1",
|
||||
"resolved": "https://registry.npmjs.org/react-share/-/react-share-4.4.1.tgz",
|
||||
@@ -20042,26 +20145,6 @@
|
||||
"react": "^16.8.3 || ^17.0.0-0 || ^18.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/react-test-renderer": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-18.3.1.tgz",
|
||||
"integrity": "sha512-KkAgygexHUkQqtvvx/otwxtuFu5cVjfzTCtjXLH9boS19/Nbtg84zS7wIQn39G8IlrhThBpQsMKkq5ZHZIYFXA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"react-is": "^18.3.1",
|
||||
"react-shallow-renderer": "^16.15.0",
|
||||
"scheduler": "^0.23.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": "^18.3.1"
|
||||
}
|
||||
},
|
||||
"node_modules/react-test-renderer/node_modules/react-is": {
|
||||
"version": "18.3.1",
|
||||
"resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz",
|
||||
"integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/react-transition-group": {
|
||||
"version": "4.4.5",
|
||||
"resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz",
|
||||
@@ -20214,6 +20297,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
|
||||
"integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"indent-string": "^4.0.0",
|
||||
@@ -20237,6 +20321,7 @@
|
||||
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
|
||||
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@babel/runtime": "^7.9.2"
|
||||
}
|
||||
@@ -20623,9 +20708,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/rtlcss": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz",
|
||||
"integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==",
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
|
||||
"integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"escalade": "^3.1.1",
|
||||
@@ -20914,6 +20999,7 @@
|
||||
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
|
||||
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fast-uri": "^3.0.1",
|
||||
@@ -21199,26 +21285,45 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp": {
|
||||
"version": "0.32.6",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
|
||||
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
|
||||
"version": "0.34.3",
|
||||
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
|
||||
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"color": "^4.2.3",
|
||||
"detect-libc": "^2.0.2",
|
||||
"node-addon-api": "^6.1.0",
|
||||
"prebuild-install": "^7.1.1",
|
||||
"semver": "^7.5.4",
|
||||
"simple-get": "^4.0.1",
|
||||
"tar-fs": "^3.0.4",
|
||||
"tunnel-agent": "^0.6.0"
|
||||
"detect-libc": "^2.0.4",
|
||||
"semver": "^7.7.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=14.15.0"
|
||||
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://opencollective.com/libvips"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@img/sharp-darwin-arm64": "0.34.3",
|
||||
"@img/sharp-darwin-x64": "0.34.3",
|
||||
"@img/sharp-libvips-darwin-arm64": "1.2.0",
|
||||
"@img/sharp-libvips-darwin-x64": "1.2.0",
|
||||
"@img/sharp-libvips-linux-arm": "1.2.0",
|
||||
"@img/sharp-libvips-linux-arm64": "1.2.0",
|
||||
"@img/sharp-libvips-linux-ppc64": "1.2.0",
|
||||
"@img/sharp-libvips-linux-s390x": "1.2.0",
|
||||
"@img/sharp-libvips-linux-x64": "1.2.0",
|
||||
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
|
||||
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
|
||||
"@img/sharp-linux-arm": "0.34.3",
|
||||
"@img/sharp-linux-arm64": "0.34.3",
|
||||
"@img/sharp-linux-ppc64": "0.34.3",
|
||||
"@img/sharp-linux-s390x": "0.34.3",
|
||||
"@img/sharp-linux-x64": "0.34.3",
|
||||
"@img/sharp-linuxmusl-arm64": "0.34.3",
|
||||
"@img/sharp-linuxmusl-x64": "0.34.3",
|
||||
"@img/sharp-wasm32": "0.34.3",
|
||||
"@img/sharp-win32-arm64": "0.34.3",
|
||||
"@img/sharp-win32-ia32": "0.34.3",
|
||||
"@img/sharp-win32-x64": "0.34.3"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/detect-libc": {
|
||||
@@ -21230,16 +21335,10 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/sharp/node_modules/node-addon-api": {
|
||||
"version": "6.1.0",
|
||||
"resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz",
|
||||
"integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/sharp/node_modules/semver": {
|
||||
"version": "7.7.1",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz",
|
||||
"integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==",
|
||||
"version": "7.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
|
||||
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
|
||||
"license": "ISC",
|
||||
"bin": {
|
||||
"semver": "bin/semver.js"
|
||||
@@ -21359,51 +21458,6 @@
|
||||
"integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==",
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/simple-concat": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz",
|
||||
"integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/simple-get": {
|
||||
"version": "4.0.1",
|
||||
"resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz",
|
||||
"integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==",
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/feross"
|
||||
},
|
||||
{
|
||||
"type": "patreon",
|
||||
"url": "https://www.patreon.com/feross"
|
||||
},
|
||||
{
|
||||
"type": "consulting",
|
||||
"url": "https://feross.org/support"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"decompress-response": "^6.0.0",
|
||||
"once": "^1.3.1",
|
||||
"simple-concat": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/simple-swizzle": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
|
||||
@@ -21744,19 +21798,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/streamx": {
|
||||
"version": "2.22.0",
|
||||
"resolved": "https://registry.npmjs.org/streamx/-/streamx-2.22.0.tgz",
|
||||
"integrity": "sha512-sLh1evHOzBy/iWRiR6d1zRcLao4gGZr3C1kzNz4fopCOKJb6xD9ub8Mpi9Mr1R6id5o43S+d93fI48UC5uM9aw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-fifo": "^1.3.2",
|
||||
"text-decoder": "^1.1.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-events": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"node_modules/strict-uri-encode": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz",
|
||||
@@ -21959,6 +22000,7 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"min-indent": "^1.0.0"
|
||||
@@ -21985,6 +22027,7 @@
|
||||
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
|
||||
"hasInstallScript": true,
|
||||
"license": "Apache-2.0",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@bundled-es-modules/deepmerge": "^4.3.1",
|
||||
"@bundled-es-modules/glob": "^10.4.2",
|
||||
@@ -22246,31 +22289,6 @@
|
||||
"node": ">=0.6"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-fs": {
|
||||
"version": "3.0.9",
|
||||
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
|
||||
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"pump": "^3.0.0",
|
||||
"tar-stream": "^3.1.5"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"bare-fs": "^4.0.1",
|
||||
"bare-path": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/tar-stream": {
|
||||
"version": "3.1.7",
|
||||
"resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.7.tgz",
|
||||
"integrity": "sha512-qJj60CXt7IU1Ffyc3NJMjh6EkuCFej46zUqJ4J7pqYlThyd9bO0XBTmcOIhSzZJVWfsLks0+nle/j538YAW9RQ==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4",
|
||||
"fast-fifo": "^1.2.0",
|
||||
"streamx": "^2.15.0"
|
||||
}
|
||||
},
|
||||
"node_modules/terser": {
|
||||
"version": "5.39.0",
|
||||
"resolved": "https://registry.npmjs.org/terser/-/terser-5.39.0.tgz",
|
||||
@@ -22391,15 +22409,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/text-decoder": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.3.tgz",
|
||||
"integrity": "sha512-3/o9z3X0X0fTupwsYvR03pJ/DjWuqqrfwBgTQzdWDiQSm9KitAyz/9WqsT2JQW7KV2m+bC2ol/zqpW37NHxLaA==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"b4a": "^1.6.4"
|
||||
}
|
||||
},
|
||||
"node_modules/text-table": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
|
||||
@@ -22493,6 +22502,7 @@
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
|
||||
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
},
|
||||
@@ -22812,7 +22822,8 @@
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
"license": "0BSD",
|
||||
"peer": true
|
||||
},
|
||||
"node_modules/tsutils": {
|
||||
"version": "3.21.0",
|
||||
@@ -22835,18 +22846,6 @@
|
||||
"integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/tunnel-agent": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz",
|
||||
"integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"safe-buffer": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/type-check": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@@ -22873,6 +22872,7 @@
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
|
||||
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
|
||||
"license": "(MIT OR CC0-1.0)",
|
||||
"peer": true,
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
},
|
||||
@@ -23642,6 +23642,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz",
|
||||
"integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/eslint-scope": "^3.7.7",
|
||||
"@types/estree": "^1.0.6",
|
||||
@@ -23744,6 +23745,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
|
||||
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@discoveryjs/json-ext": "^0.5.0",
|
||||
"@webpack-cli/configtest": "^2.1.1",
|
||||
@@ -23830,6 +23832,7 @@
|
||||
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
|
||||
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
|
||||
"license": "MIT",
|
||||
"peer": true,
|
||||
"dependencies": {
|
||||
"@types/bonjour": "^3.5.9",
|
||||
"@types/connect-history-api-fallback": "^1.3.5",
|
||||
|
||||
11
package.json
@@ -15,7 +15,6 @@
|
||||
"i18n_extract": "fedx-scripts formatjs extract",
|
||||
"lint": "fedx-scripts eslint --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"lint:fix": "fedx-scripts eslint --fix --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"snapshot": "fedx-scripts jest --updateSnapshot",
|
||||
"start": "fedx-scripts webpack-dev-server --progress",
|
||||
"start:with-theme": "paragon install-theme && npm start && npm install",
|
||||
"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",
|
||||
@@ -36,17 +35,16 @@
|
||||
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
|
||||
"@edx/browserslist-config": "1.5.0",
|
||||
"@edx/frontend-component-footer": "^14.6.0",
|
||||
"@edx/frontend-component-header": "^6.2.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.24.0",
|
||||
"@edx/frontend-component-header": "^8.0.0",
|
||||
"@edx/frontend-lib-learning-assistant": "^2.23.1",
|
||||
"@edx/frontend-lib-special-exams": "^4.0.0",
|
||||
"@edx/frontend-platform": "^8.3.1",
|
||||
"@edx/frontend-platform": "^8.4.0",
|
||||
"@edx/openedx-atlas": "^0.7.0",
|
||||
"@edx/react-unit-test-utils": "^4.0.0",
|
||||
"@fortawesome/free-brands-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-regular-svg-icons": "5.15.4",
|
||||
"@fortawesome/free-solid-svg-icons": "5.15.4",
|
||||
"@fortawesome/react-fontawesome": "^0.1.4",
|
||||
"@openedx/frontend-build": "^14.5.0",
|
||||
"@openedx/frontend-build": "^14.6.2",
|
||||
"@openedx/frontend-plugin-framework": "^1.7.0",
|
||||
"@openedx/paragon": "^23.4.5",
|
||||
"@popperjs/core": "2.11.8",
|
||||
@@ -76,7 +74,6 @@
|
||||
"truncate-html": "1.0.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@edx/reactifex": "2.2.0",
|
||||
"@pact-foundation/pact": "^13.0.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
|
||||
@@ -1,228 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<ErrorPage
|
||||
message="test-error-message"
|
||||
/>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
|
||||
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
|
||||
<React Strict Mode>
|
||||
<AppProvider
|
||||
store={
|
||||
{
|
||||
"dispatch": [Function],
|
||||
"getState": [Function],
|
||||
"replaceReducer": [Function],
|
||||
"subscribe": [Function],
|
||||
Symbol(Symbol.observable): [Function],
|
||||
}
|
||||
}
|
||||
>
|
||||
<HelmetWrapper
|
||||
defer={true}
|
||||
encodeSpecialCharacters={true}
|
||||
>
|
||||
<link
|
||||
href="favicon-url"
|
||||
rel="shortcut icon"
|
||||
type="image/x-icon"
|
||||
/>
|
||||
</HelmetWrapper>
|
||||
<PathFixesProvider>
|
||||
<NoticesProvider>
|
||||
<UserMessagesProvider>
|
||||
<div
|
||||
className="app-container"
|
||||
>
|
||||
<Routes>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Page Not Found />
|
||||
</PageWrap>
|
||||
}
|
||||
path="*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Goal Unsubscribe />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/goal-unsubscribe/:token"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Courseware Redirect Landing Page />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/redirect/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<PageWrap>
|
||||
<Preferences Unsubscribe />
|
||||
</PageWrap>
|
||||
}
|
||||
path="/preferences-unsubscribe/:userToken/:updatePatch?"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Course Access Error Page />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/access-denied"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="outline"
|
||||
>
|
||||
<Outline Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/home"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="lti_live"
|
||||
>
|
||||
<Live Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/live"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="dates"
|
||||
>
|
||||
<Dates Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/dates"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseHome"
|
||||
tab="discussion"
|
||||
>
|
||||
<Discussion Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/discussion/:path/*"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
isProgressTab={true}
|
||||
slice="courseHome"
|
||||
tab="progress"
|
||||
>
|
||||
<Progress Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/progress/:targetUserId/"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
isProgressTab={true}
|
||||
slice="courseHome"
|
||||
tab="progress"
|
||||
>
|
||||
<Progress Tab />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/progress"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Tab Container
|
||||
fetch={[Function]}
|
||||
slice="courseware"
|
||||
tab="courseware"
|
||||
>
|
||||
<Course Exit />
|
||||
</Tab Container>
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/course-end"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/:sequenceId/:unitId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId/:sequenceId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/course/:courseId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/preview/course/:courseId/:sequenceId/:unitId"
|
||||
/>
|
||||
<Route
|
||||
element={
|
||||
<DecodePageRoute>
|
||||
<Courseware Container />
|
||||
</DecodePageRoute>
|
||||
}
|
||||
path="/preview/course/:courseId/:sequenceId"
|
||||
/>
|
||||
</Routes>
|
||||
</div>
|
||||
</UserMessagesProvider>
|
||||
</NoticesProvider>
|
||||
</PathFixesProvider>
|
||||
</AppProvider>
|
||||
</React Strict Mode>
|
||||
`;
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
screen,
|
||||
} from '../../setupTest';
|
||||
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
|
||||
import messages from './messages';
|
||||
|
||||
function renderComponent() {
|
||||
const { container } = render(<CoursewareSearchEmpty />);
|
||||
@@ -16,9 +17,12 @@ describe('CoursewareSearchEmpty', () => {
|
||||
initializeMockApp();
|
||||
});
|
||||
|
||||
it('should match the snapshot', () => {
|
||||
it('render empty results text and corresponding classes', () => {
|
||||
renderComponent();
|
||||
|
||||
expect(screen.getByTestId('no-results')).toMatchSnapshot();
|
||||
const emptyText = screen.getByText(messages.searchResultsNone.defaultMessage);
|
||||
expect(emptyText).toBeInTheDocument();
|
||||
expect(emptyText).toHaveClass('courseware-search-results__empty');
|
||||
expect(emptyText).toHaveAttribute('data-testid', 'no-results');
|
||||
expect(emptyText.parentElement).toHaveClass('courseware-search-results');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
import CoursewareSearchResults from './CoursewareSearchResults';
|
||||
import messages from './messages';
|
||||
import searchResultsFactory from './test-data/search-results-factory';
|
||||
import * as mock from './test-data/mocked-response.json';
|
||||
|
||||
jest.mock('react-redux');
|
||||
|
||||
@@ -34,8 +35,53 @@ describe('CoursewareSearchResults', () => {
|
||||
renderComponent({ results });
|
||||
});
|
||||
|
||||
it('should match the snapshot', () => {
|
||||
expect(screen.getByTestId('search-results')).toMatchSnapshot();
|
||||
it('should render complete list', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
expect(courses.length).toBe(mock.results.length);
|
||||
});
|
||||
|
||||
it('should render correct link for internal course', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
const firstCourse = courses[0];
|
||||
const firstCourseTitle = firstCourse.querySelector('.courseware-search-results__title span');
|
||||
expect(firstCourseTitle.innerHTML).toEqual(mock.results[0].data.content.display_name);
|
||||
expect(firstCourse.href).toContain(mock.results[0].data.url);
|
||||
expect(firstCourse).not.toHaveAttribute('target', '_blank');
|
||||
expect(firstCourse).not.toHaveAttribute('rel', 'nofollow');
|
||||
});
|
||||
|
||||
it('should render correct link if is External url course', () => {
|
||||
const courses = screen.getAllByRole('link');
|
||||
const externalCourse = courses[courses.length - 1];
|
||||
const externalCourseTitle = externalCourse.querySelector('.courseware-search-results__title span');
|
||||
expect(externalCourseTitle.innerHTML).toEqual(mock.results[mock.results.length - 1].data.content.display_name);
|
||||
expect(externalCourse.href).toContain(mock.results[mock.results.length - 1].data.url);
|
||||
expect(externalCourse).toHaveAttribute('target', '_blank');
|
||||
expect(externalCourse).toHaveAttribute('rel', 'nofollow');
|
||||
const icon = externalCourse.querySelector('svg');
|
||||
expect(icon).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should render location breadcrumbs', () => {
|
||||
const breadcrumbs = screen.getAllByText(mock.results[0].data.location[0]);
|
||||
expect(breadcrumbs.length).toBeGreaterThan(0);
|
||||
const firstBreadcrumb = breadcrumbs[0].closest('li');
|
||||
expect(firstBreadcrumb).toBeInTheDocument();
|
||||
expect(firstBreadcrumb.querySelector('div').textContent).toBe(mock.results[0].data.location[0]);
|
||||
expect(firstBreadcrumb.nextSibling.querySelector('div').textContent).toBe(mock.results[0].data.location[1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when results are provided with content hits', () => {
|
||||
beforeEach(() => {
|
||||
const { results } = searchResultsFactory('Passing');
|
||||
renderComponent({ results });
|
||||
});
|
||||
|
||||
it('should render content hits', () => {
|
||||
const contentHits = screen.getByText('1');
|
||||
expect(contentHits).toBeInTheDocument();
|
||||
expect(contentHits.tagName).toBe('EM');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -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>
|
||||
`;
|
||||
@@ -1,306 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`mapSearchResponse when the response is correct should match snapshot 1`] = `
|
||||
{
|
||||
"filters": [
|
||||
{
|
||||
"count": 7,
|
||||
"key": "capa",
|
||||
"label": "CAPA",
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"key": "sequence",
|
||||
"label": "Sequence",
|
||||
},
|
||||
{
|
||||
"count": 9,
|
||||
"key": "text",
|
||||
"label": "Text",
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
"key": "unknown",
|
||||
"label": "Unknown",
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
"key": "video",
|
||||
"label": "Video",
|
||||
},
|
||||
],
|
||||
"maxScore": 3.4545178,
|
||||
"ms": 5,
|
||||
"results": [
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Demo Course Overview",
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@sequential+block@edx_introduction",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Passing a Course",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@5e009378f0b64585baa0a14b155974b9",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Passing a Course",
|
||||
],
|
||||
"score": 3.4545178,
|
||||
"title": "Passing a Course",
|
||||
"type": "sequence",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@vertical+block@c7e98fd39a6944edb6b286c32e1150ff",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Text input",
|
||||
],
|
||||
"score": 1.5874016,
|
||||
"title": "Text Input",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@0d759dee4f9d459c8956136dbde55f02",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Pointing on a Picture",
|
||||
],
|
||||
"score": 1.5499392,
|
||||
"title": "Pointing on a Picture",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@c554538a57664fac80783b99d9d6da7c",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Getting Answers",
|
||||
],
|
||||
"score": 1.5003732,
|
||||
"title": "Getting Answers",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@45d46192272c4f6db6b63586520bbdf4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
],
|
||||
"score": 1.4792063,
|
||||
"title": "Welcome!",
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@0b9e39477cf34507a7a48f74be381fdd",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Multiple Choice Questions",
|
||||
],
|
||||
"score": 1.4341705,
|
||||
"title": "Multiple Choice Questions",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@a0effb954cca4759994f1ac9e9434bf4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Homework - Question Styles",
|
||||
"Numerical Input",
|
||||
],
|
||||
"score": 1.2987298,
|
||||
"title": "Numerical Input",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@75f9562c77bc4858b61f907bb810d974",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Video Presentation Styles",
|
||||
],
|
||||
"score": 1.1870136,
|
||||
"title": "Connecting a Circuit and a Circuit Diagram",
|
||||
"type": "video",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@video+block@636541acbae448d98ab484b028c9a7f6",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
"location": [
|
||||
"Example Week 2: Get Interactive",
|
||||
"Homework - Labs and Demos",
|
||||
"Code Grader",
|
||||
],
|
||||
"score": 1.0107487,
|
||||
"title": "CAPA",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@python_grader",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"Interactive Questions",
|
||||
],
|
||||
"score": 0.96387196,
|
||||
"title": "Interactive Questions",
|
||||
"type": "capa",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@problem+block@9cee77a606ea4c1aa5440e0ea5d0f618",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
"location": [
|
||||
"Introduction",
|
||||
"Demo Course Overview",
|
||||
"Introduction: Video and Sequences",
|
||||
],
|
||||
"score": 0.8844358,
|
||||
"title": "Blank HTML Page",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@030e35c4756a4ddc8d40b95fbbfff4d4",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Discussion Forums",
|
||||
],
|
||||
"score": 0.8803684,
|
||||
"title": "Discussion Forums",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@html_49b4494da2f7",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"Overall Grade Performance",
|
||||
],
|
||||
"score": 0.87981963,
|
||||
"title": "Overall Grade",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f4a39219742149f781a1dda6f43a623c",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
],
|
||||
"score": 0.84284115,
|
||||
"title": "Blank HTML Page",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@87fa6792d79f4862be098e5169e93339",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Homework - Find Your Study Buddy",
|
||||
"Homework - Find Your Study Buddy",
|
||||
],
|
||||
"score": 0.84284115,
|
||||
"title": "Find Your Study Buddy",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@6018785795994726950614ce7d0f38c5",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
"location": [
|
||||
"Example Week 3: Be Social",
|
||||
"Lesson 3 - Be Social",
|
||||
"Be Social",
|
||||
],
|
||||
"score": 0.84210813,
|
||||
"title": "Be Social",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@f9f3a25e7bab46e583fd1fbbd7a2f6a0",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
"location": [
|
||||
"About Exams and Certificates",
|
||||
"edX Exams",
|
||||
"EdX Exams",
|
||||
],
|
||||
"score": 0.8306555,
|
||||
"title": "EdX Exams",
|
||||
"type": "text",
|
||||
"url": "/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+DemoX+Demo_Course+type@html+block@8293139743f34377817d537b69911530",
|
||||
},
|
||||
{
|
||||
"contentHits": 0,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@html+block@9d5104b502f24ee89c3d2f4ce9d347cf",
|
||||
"location": [
|
||||
"Example Week 1: Getting Started",
|
||||
"Lesson 1 - Getting Started",
|
||||
"When Are Your Exams? ",
|
||||
],
|
||||
"score": 0.82610154,
|
||||
"title": "When Are Your Exams? ",
|
||||
"type": "text",
|
||||
"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,
|
||||
}
|
||||
`;
|
||||
@@ -10,8 +10,8 @@ describe('mapSearchResponse', () => {
|
||||
response = mapSearchResponse(camelCaseObject(mockedResponse));
|
||||
});
|
||||
|
||||
it('should match snapshot', () => {
|
||||
expect(response).toMatchSnapshot();
|
||||
it('should match number of results', () => {
|
||||
expect(response.results.length).toBe(mockedResponse.results.length);
|
||||
});
|
||||
|
||||
it('should match expected filters', () => {
|
||||
@@ -24,6 +24,25 @@ describe('mapSearchResponse', () => {
|
||||
];
|
||||
expect(response.filters).toEqual(expectedFilters);
|
||||
});
|
||||
|
||||
it('should match expected results', () => {
|
||||
const mockFirstResult = mockedResponse.results[0];
|
||||
const expectedFirstResult = {
|
||||
id: mockFirstResult.data.id,
|
||||
title: mockFirstResult.data.content.display_name,
|
||||
type: mockFirstResult.data.content_type.toLowerCase(),
|
||||
location: mockFirstResult.data.location,
|
||||
url: mockFirstResult.data.url,
|
||||
contentHits: 0,
|
||||
score: mockFirstResult.score,
|
||||
};
|
||||
expect(response.results[0]).toEqual(expectedFirstResult);
|
||||
});
|
||||
|
||||
it('should match expected ms and max score', () => {
|
||||
expect(response.maxScore).toBe(mockedResponse.max_score);
|
||||
expect(response.ms).toBe(mockedResponse.took);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the a keyword is provided', () => {
|
||||
|
||||
@@ -17,21 +17,7 @@ Factory.define('progressTabData')
|
||||
percent: 1,
|
||||
is_passing: true,
|
||||
},
|
||||
final_grades: 0.5,
|
||||
credit_course_requirements: null,
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
short_label: 'HW',
|
||||
weight: 1,
|
||||
average_grade: 1,
|
||||
weighted_grade: 1,
|
||||
num_droppable: 1,
|
||||
num_total: 2,
|
||||
has_hidden_contribution: 'none',
|
||||
last_grade_publish_date: null,
|
||||
},
|
||||
],
|
||||
section_scores: [
|
||||
{
|
||||
display_name: 'First section',
|
||||
|
||||
@@ -1,958 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`Data layer integration tests Test fetchDatesTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"examsData": null,
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"dates": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"courseDateBlocks": [
|
||||
{
|
||||
"date": "2020-05-01T17:59:41Z",
|
||||
"dateType": "course-start-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "",
|
||||
"title": "Course Starts",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-04T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Completed",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-05T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Multi Badges Past Due",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-27T02:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Past Due 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2020-05-28T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Completed/Due 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"complete": true,
|
||||
"date": "2020-05-29T08:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Completed 2",
|
||||
},
|
||||
{
|
||||
"date": "2020-06-16T17:59:40.942669Z",
|
||||
"dateType": "verified-upgrade-deadline",
|
||||
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "Upgrade to Verified Certificate",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-17T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": "ORA Dates are set by the instructor, and can't be changed",
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "ORA Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-18T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Both Verified 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"learnerHasAccess": true,
|
||||
"title": "One Unreleased 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-19T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "https://example.com/",
|
||||
"title": "One Unreleased 2",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 1",
|
||||
},
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"date": "2030-08-20T05:59:40.942669Z",
|
||||
"dateType": "assignment-due-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"title": "Both Unreleased 2",
|
||||
},
|
||||
{
|
||||
"date": "2030-08-23T00:00:00Z",
|
||||
"dateType": "course-end-date",
|
||||
"description": "",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": true,
|
||||
"link": "",
|
||||
"title": "Course Ends",
|
||||
},
|
||||
{
|
||||
"date": "2030-09-01T00:00:00Z",
|
||||
"dateType": "verification-deadline-date",
|
||||
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
|
||||
"extraInfo": null,
|
||||
"learnerHasAccess": false,
|
||||
"link": "https://example.com/",
|
||||
"title": "Verification Deadline",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
"hasEnded": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"learnerIsFullAccess": true,
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"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,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"examsData": null,
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"outline": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"certData": {
|
||||
"certStatus": null,
|
||||
"certWebViewUrl": null,
|
||||
"certificateAvailableDate": null,
|
||||
},
|
||||
"courseBlocks": {
|
||||
"courses": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
],
|
||||
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
|
||||
},
|
||||
},
|
||||
"sections": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
|
||||
"complete": false,
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"hideFromTOC": undefined,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"resumeBlock": false,
|
||||
"sequenceIds": [
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
],
|
||||
"title": "Title of Section",
|
||||
},
|
||||
},
|
||||
"sequences": {
|
||||
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
|
||||
"complete": false,
|
||||
"description": null,
|
||||
"due": null,
|
||||
"effortActivities": 2,
|
||||
"effortTime": 15,
|
||||
"hideFromTOC": undefined,
|
||||
"icon": null,
|
||||
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
|
||||
"isPreview": false,
|
||||
"navigationDisabled": undefined,
|
||||
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
|
||||
"showLink": true,
|
||||
"title": "Title of Sequence",
|
||||
},
|
||||
},
|
||||
},
|
||||
"courseGoals": {
|
||||
"daysPerWeek": null,
|
||||
"goalOptions": [],
|
||||
"selectedGoal": null,
|
||||
"subscribedToReminders": null,
|
||||
"weeklyLearningGoalEnabled": false,
|
||||
},
|
||||
"courseTools": [
|
||||
{
|
||||
"analyticsId": "edx.bookmarks",
|
||||
"title": "Bookmarks",
|
||||
"url": "https://example.com/bookmarks",
|
||||
},
|
||||
],
|
||||
"datesBannerInfo": {
|
||||
"contentTypeGatingEnabled": false,
|
||||
"missedDeadlines": false,
|
||||
"missedGatedContent": false,
|
||||
},
|
||||
"datesWidget": {
|
||||
"courseDateBlocks": [],
|
||||
},
|
||||
"enableProctoredExams": undefined,
|
||||
"enrollAlert": {
|
||||
"canEnroll": true,
|
||||
"extraText": "Contact the administrator.",
|
||||
},
|
||||
"enrollmentMode": undefined,
|
||||
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
|
||||
"hasEnded": undefined,
|
||||
"hasScheduledContent": null,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"offer": null,
|
||||
"resumeCourse": {
|
||||
"hasVisitedCourse": false,
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
|
||||
},
|
||||
"timeOffsetMillis": 0,
|
||||
"userHasPassingGrade": undefined,
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": "2050-01-01T12:00:00",
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "ABCD1234",
|
||||
"upgradeUrl": "http://localhost:18000/dashboard",
|
||||
},
|
||||
"welcomeMessageHtml": "<p>Welcome to this course!</p>",
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"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,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
|
||||
{
|
||||
"courseHome": {
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"courseStatus": "loaded",
|
||||
"examsData": null,
|
||||
"proctoringPanelStatus": "loading",
|
||||
"showSearch": false,
|
||||
"targetUserId": undefined,
|
||||
"toastBodyLink": null,
|
||||
"toastBodyText": null,
|
||||
"toastHeader": "",
|
||||
},
|
||||
"courseware": {
|
||||
"courseId": null,
|
||||
"courseOutline": {},
|
||||
"courseOutlineShouldUpdate": false,
|
||||
"courseOutlineStatus": "loading",
|
||||
"courseStatus": "loading",
|
||||
"coursewareOutlineSidebarSettings": {},
|
||||
"sequenceId": null,
|
||||
"sequenceMightBeUnit": false,
|
||||
"sequenceStatus": "loading",
|
||||
},
|
||||
"learningAssistant": ObjectContaining {
|
||||
"conversationId": Any<String>,
|
||||
},
|
||||
"models": {
|
||||
"courseHomeMeta": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"canViewCertificate": true,
|
||||
"celebrations": null,
|
||||
"courseAccess": {
|
||||
"additionalContextUserMessage": null,
|
||||
"developerMessage": null,
|
||||
"errorCode": null,
|
||||
"hasAccess": true,
|
||||
"userFragment": null,
|
||||
"userMessage": null,
|
||||
},
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"isEnrolled": false,
|
||||
"isMasquerading": false,
|
||||
"isNewDiscussionSidebarViewEnabled": false,
|
||||
"isSelfPaced": false,
|
||||
"isStaff": false,
|
||||
"number": "DemoX",
|
||||
"org": "edX",
|
||||
"originalUserIsStaff": false,
|
||||
"start": "2013-02-05T05:00:00Z",
|
||||
"tabs": [
|
||||
{
|
||||
"slug": "outline",
|
||||
"title": "Course",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
|
||||
},
|
||||
{
|
||||
"slug": "discussion",
|
||||
"title": "Discussion",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
|
||||
},
|
||||
{
|
||||
"slug": "wiki",
|
||||
"title": "Wiki",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
|
||||
},
|
||||
{
|
||||
"slug": "progress",
|
||||
"title": "Progress",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
|
||||
},
|
||||
{
|
||||
"slug": "instructor",
|
||||
"title": "Instructor",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
|
||||
},
|
||||
{
|
||||
"slug": "dates",
|
||||
"title": "Dates",
|
||||
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
|
||||
},
|
||||
],
|
||||
"title": "Demonstration Course",
|
||||
"userTimezone": "UTC",
|
||||
"username": "MockUser",
|
||||
"verifiedMode": {
|
||||
"accessExpirationDate": null,
|
||||
"currency": "USD",
|
||||
"currencySymbol": "$",
|
||||
"price": 149,
|
||||
"sku": "8CF08E5",
|
||||
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"progress": {
|
||||
"course-v1:edX+DemoX+Demo_Course": {
|
||||
"accessExpiration": null,
|
||||
"assignmentTypeGradeSummary": [
|
||||
{
|
||||
"averageGrade": 1,
|
||||
"hasHiddenContribution": "none",
|
||||
"lastGradePublishDate": null,
|
||||
"numDroppable": 1,
|
||||
"numTotal": 2,
|
||||
"shortLabel": "HW",
|
||||
"type": "Homework",
|
||||
"weight": 1,
|
||||
"weightedGrade": 1,
|
||||
},
|
||||
],
|
||||
"certificateData": {},
|
||||
"completionSummary": {
|
||||
"completeCount": 1,
|
||||
"incompleteCount": 1,
|
||||
"lockedCount": 0,
|
||||
},
|
||||
"courseGrade": {
|
||||
"isPassing": true,
|
||||
"letterGrade": "pass",
|
||||
"percent": 1,
|
||||
},
|
||||
"courseId": "course-v1:edX+DemoX+Demo_Course",
|
||||
"creditCourseRequirements": null,
|
||||
"end": "3027-03-31T00:00:00Z",
|
||||
"enrollmentMode": "audit",
|
||||
"finalGrades": 0.5,
|
||||
"gradesFeatureIsFullyLocked": false,
|
||||
"gradesFeatureIsPartiallyLocked": false,
|
||||
"gradingPolicy": {
|
||||
"assignmentPolicies": [
|
||||
{
|
||||
"numDroppable": 1,
|
||||
"numTotal": 2,
|
||||
"shortLabel": "HW",
|
||||
"type": "Homework",
|
||||
"weight": 1,
|
||||
},
|
||||
],
|
||||
"gradeRange": {
|
||||
"pass": 0.75,
|
||||
},
|
||||
},
|
||||
"hasScheduledContent": false,
|
||||
"id": "course-v1:edX+DemoX+Demo_Course",
|
||||
"sectionScores": [
|
||||
{
|
||||
"displayName": "First section",
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"blockKey": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345",
|
||||
"displayName": "First subsection",
|
||||
"hasGradedAssignment": true,
|
||||
"learnerHasAccess": true,
|
||||
"numPointsEarned": 0,
|
||||
"numPointsPossible": 3,
|
||||
"percentGraded": 0,
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
{
|
||||
"earned": 0,
|
||||
"possible": 1,
|
||||
},
|
||||
],
|
||||
"showCorrectness": "always",
|
||||
"showGrades": true,
|
||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/first_subsection",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
"displayName": "Second section",
|
||||
"subsections": [
|
||||
{
|
||||
"assignmentType": "Homework",
|
||||
"displayName": "Second subsection",
|
||||
"hasGradedAssignment": true,
|
||||
"numPointsEarned": 1,
|
||||
"numPointsPossible": 1,
|
||||
"percentGraded": 1,
|
||||
"problemScores": [
|
||||
{
|
||||
"earned": 1,
|
||||
"possible": 1,
|
||||
},
|
||||
],
|
||||
"showCorrectness": "always",
|
||||
"showGrades": true,
|
||||
"url": "http://learning.edx.org/course/course-v1:edX+Test+run/second_subsection",
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
"studioUrl": "http://studio.edx.org/settings/grading/course-v1:edX+Test+run",
|
||||
"userHasPassingGrade": false,
|
||||
"verificationData": {
|
||||
"link": null,
|
||||
"status": "none",
|
||||
"statusDate": null,
|
||||
},
|
||||
"verifiedMode": null,
|
||||
},
|
||||
},
|
||||
},
|
||||
"plugins": {},
|
||||
"recommendations": {
|
||||
"recommendationsStatus": "loading",
|
||||
},
|
||||
"specialExams": {
|
||||
"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,
|
||||
"showExistingUserCourseHomeTour": false,
|
||||
"showNewUserCourseHomeModal": false,
|
||||
"showNewUserCourseHomeTour": false,
|
||||
"toursEnabled": false,
|
||||
},
|
||||
}
|
||||
`;
|
||||
@@ -3,6 +3,93 @@ import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { logInfo } from '@edx/frontend-platform/logging';
|
||||
import { appendBrowserTimezoneToUrl } from '../../utils';
|
||||
|
||||
const calculateAssignmentTypeGrades = (points, assignmentWeight, numDroppable) => {
|
||||
let dropCount = numDroppable;
|
||||
// Drop the lowest grades
|
||||
while (dropCount && points.length >= dropCount) {
|
||||
const lowestScore = Math.min(...points);
|
||||
const lowestScoreIndex = points.indexOf(lowestScore);
|
||||
points.splice(lowestScoreIndex, 1);
|
||||
dropCount--;
|
||||
}
|
||||
let averageGrade = 0;
|
||||
let weightedGrade = 0;
|
||||
if (points.length) {
|
||||
// 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
|
||||
// exists in edx-platform.
|
||||
averageGrade = (points.reduce((a, b) => a + b, 0) / points.length).toFixed(4);
|
||||
weightedGrade = averageGrade * assignmentWeight;
|
||||
}
|
||||
return { averageGrade, weightedGrade };
|
||||
};
|
||||
|
||||
function normalizeAssignmentPolicies(assignmentPolicies, sectionScores) {
|
||||
const gradeByAssignmentType = {};
|
||||
assignmentPolicies.forEach(assignment => {
|
||||
// Create an array with the number of total assignments and set the scores to 0
|
||||
// as placeholders for assignments that have not yet been released
|
||||
gradeByAssignmentType[assignment.type] = {
|
||||
grades: Array(assignment.numTotal).fill(0),
|
||||
numAssignmentsCreated: 0,
|
||||
numTotalExpectedAssignments: assignment.numTotal,
|
||||
};
|
||||
});
|
||||
|
||||
sectionScores.forEach((chapter) => {
|
||||
chapter.subsections.forEach((subsection) => {
|
||||
if (!(subsection.hasGradedAssignment && subsection.showGrades && subsection.numPointsPossible)) {
|
||||
return;
|
||||
}
|
||||
const {
|
||||
assignmentType,
|
||||
numPointsEarned,
|
||||
numPointsPossible,
|
||||
} = subsection;
|
||||
|
||||
// If a subsection's assignment type does not match an assignment policy in Studio,
|
||||
// we won't be able to include it in this accumulation of grades by assignment type.
|
||||
// This may happen if a course author has removed/renamed an assignment policy in Studio and
|
||||
// neglected to update the subsection's of that assignment type
|
||||
if (!gradeByAssignmentType[assignmentType]) {
|
||||
return;
|
||||
}
|
||||
|
||||
let {
|
||||
numAssignmentsCreated,
|
||||
} = gradeByAssignmentType[assignmentType];
|
||||
|
||||
numAssignmentsCreated++;
|
||||
if (numAssignmentsCreated <= gradeByAssignmentType[assignmentType].numTotalExpectedAssignments) {
|
||||
// Remove a placeholder grade so long as the number of recorded created assignments is less than the number
|
||||
// of expected assignments
|
||||
gradeByAssignmentType[assignmentType].grades.shift();
|
||||
}
|
||||
// Add the graded assignment to the list
|
||||
gradeByAssignmentType[assignmentType].grades.push(numPointsEarned ? numPointsEarned / numPointsPossible : 0);
|
||||
// Record the created assignment
|
||||
gradeByAssignmentType[assignmentType].numAssignmentsCreated = numAssignmentsCreated;
|
||||
});
|
||||
});
|
||||
|
||||
return assignmentPolicies.map((assignment) => {
|
||||
const { averageGrade, weightedGrade } = calculateAssignmentTypeGrades(
|
||||
gradeByAssignmentType[assignment.type].grades,
|
||||
assignment.weight,
|
||||
assignment.numDroppable,
|
||||
);
|
||||
|
||||
return {
|
||||
averageGrade,
|
||||
numDroppable: assignment.numDroppable,
|
||||
shortLabel: assignment.shortLabel,
|
||||
type: assignment.type,
|
||||
weight: assignment.weight,
|
||||
weightedGrade,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Tweak the metadata for consistency
|
||||
* @param metadata the data to normalize
|
||||
@@ -68,7 +155,6 @@ export function normalizeOutlineBlocks(courseId, blocks) {
|
||||
title: block.display_name,
|
||||
hideFromTOC: block.hide_from_toc,
|
||||
navigationDisabled: block.navigation_disabled,
|
||||
isPreview: block.is_preview,
|
||||
};
|
||||
break;
|
||||
|
||||
@@ -150,6 +236,11 @@ export async function getProgressTabData(courseId, targetUserId) {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
const camelCasedData = camelCaseObject(data);
|
||||
|
||||
camelCasedData.gradingPolicy.assignmentPolicies = normalizeAssignmentPolicies(
|
||||
camelCasedData.gradingPolicy.assignmentPolicies,
|
||||
camelCasedData.sectionScores,
|
||||
);
|
||||
|
||||
// We replace gradingPolicy.gradeRange with the original data to preserve the intended casing for the grade.
|
||||
// For example, if a grade range key is "A", we do not want it to be camel cased (i.e. "A" would become "a")
|
||||
// in order to preserve a course team's desired grade formatting.
|
||||
@@ -380,24 +471,3 @@ export async function searchCourseContentFromAPI(courseId, searchKeyword, option
|
||||
|
||||
return camelCaseObject(response);
|
||||
}
|
||||
|
||||
export async function getExamsData(courseId, sequenceId) {
|
||||
let url;
|
||||
|
||||
if (!getConfig().EXAMS_BASE_URL) {
|
||||
url = `${getConfig().LMS_BASE_URL}/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||
} else {
|
||||
url = `${getConfig().EXAMS_BASE_URL}/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||
}
|
||||
|
||||
try {
|
||||
const { data } = await getAuthenticatedHttpClient().get(url);
|
||||
return camelCaseObject(data);
|
||||
} catch (error) {
|
||||
const { httpErrorStatus } = error && error.customAttributes;
|
||||
if (httpErrorStatus === 404) {
|
||||
return {};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,4 @@
|
||||
import { getConfig, setConfig } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import MockAdapter from 'axios-mock-adapter';
|
||||
import { getTimeOffsetMillis, getExamsData } from './api';
|
||||
import { initializeMockApp } from '../../setupTest';
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
const axiosMock = new MockAdapter(getAuthenticatedHttpClient());
|
||||
import { getTimeOffsetMillis } from './api';
|
||||
|
||||
describe('Calculate the time offset properly', () => {
|
||||
it('Should return 0 if the headerDate is not set', async () => {
|
||||
@@ -22,156 +14,3 @@ describe('Calculate the time offset properly', () => {
|
||||
expect(offset).toBe(86398750);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getExamsData', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceId = 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345';
|
||||
let originalConfig;
|
||||
|
||||
beforeEach(() => {
|
||||
axiosMock.reset();
|
||||
originalConfig = getConfig();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
axiosMock.reset();
|
||||
if (originalConfig) {
|
||||
setConfig(originalConfig);
|
||||
}
|
||||
});
|
||||
|
||||
it('should use LMS URL when EXAMS_BASE_URL is not configured', async () => {
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: undefined,
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
});
|
||||
|
||||
const mockExamData = {
|
||||
exam: {
|
||||
id: 1,
|
||||
course_id: courseId,
|
||||
content_id: sequenceId,
|
||||
exam_name: 'Test Exam',
|
||||
attempt_status: 'created',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||
|
||||
const result = await getExamsData(courseId, sequenceId);
|
||||
|
||||
expect(result).toEqual({
|
||||
exam: {
|
||||
id: 1,
|
||||
courseId,
|
||||
contentId: sequenceId,
|
||||
examName: 'Test Exam',
|
||||
attemptStatus: 'created',
|
||||
},
|
||||
});
|
||||
expect(axiosMock.history.get).toHaveLength(1);
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||
});
|
||||
|
||||
it('should use EXAMS_BASE_URL when configured', async () => {
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
});
|
||||
|
||||
const mockExamData = {
|
||||
exam: {
|
||||
id: 1,
|
||||
course_id: courseId,
|
||||
content_id: sequenceId,
|
||||
exam_name: 'Test Exam',
|
||||
attempt_status: 'submitted',
|
||||
},
|
||||
};
|
||||
|
||||
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}`;
|
||||
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||
|
||||
const result = await getExamsData(courseId, sequenceId);
|
||||
|
||||
expect(result).toEqual({
|
||||
exam: {
|
||||
id: 1,
|
||||
courseId,
|
||||
contentId: sequenceId,
|
||||
examName: 'Test Exam',
|
||||
attemptStatus: 'submitted',
|
||||
},
|
||||
});
|
||||
expect(axiosMock.history.get).toHaveLength(1);
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||
});
|
||||
|
||||
it('should return empty object when API returns 404', async () => {
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: undefined,
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
});
|
||||
|
||||
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||
|
||||
// Mock a 404 error with the custom error response function to add customAttributes
|
||||
axiosMock.onGet(expectedUrl).reply(() => {
|
||||
const error = new Error('Request failed with status code 404');
|
||||
error.response = { status: 404, data: {} };
|
||||
error.customAttributes = { httpErrorStatus: 404 };
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
const result = await getExamsData(courseId, sequenceId);
|
||||
|
||||
expect(result).toEqual({});
|
||||
expect(axiosMock.history.get).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should throw error for non-404 HTTP errors', async () => {
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: undefined,
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
});
|
||||
|
||||
const expectedUrl = `http://localhost:18000/api/edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}?is_learning_mfe=true&content_id=${encodeURIComponent(sequenceId)}`;
|
||||
|
||||
// Mock a 500 error with custom error response
|
||||
axiosMock.onGet(expectedUrl).reply(() => {
|
||||
const error = new Error('Request failed with status code 500');
|
||||
error.response = { status: 500, data: { error: 'Server Error' } };
|
||||
error.customAttributes = { httpErrorStatus: 500 };
|
||||
return Promise.reject(error);
|
||||
});
|
||||
|
||||
await expect(getExamsData(courseId, sequenceId)).rejects.toThrow();
|
||||
expect(axiosMock.history.get).toHaveLength(1);
|
||||
});
|
||||
|
||||
it('should properly encode URL parameters', async () => {
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||
LMS_BASE_URL: 'http://localhost:18000',
|
||||
});
|
||||
|
||||
const specialCourseId = 'course-v1:edX+Demo X+Demo Course';
|
||||
const specialSequenceId = 'block-v1:edX+Demo X+Demo Course+type@sequential+block@test sequence';
|
||||
|
||||
const mockExamData = { exam: { id: 1 } };
|
||||
const expectedUrl = `http://localhost:18740/api/v1/student/exam/attempt/course_id/${encodeURIComponent(specialCourseId)}/content_id/${encodeURIComponent(specialSequenceId)}`;
|
||||
axiosMock.onGet(expectedUrl).reply(200, mockExamData);
|
||||
|
||||
await getExamsData(specialCourseId, specialSequenceId);
|
||||
|
||||
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
|
||||
expect(axiosMock.history.get[0].url).toContain('course-v1%3AedX%2BDemo%20X%2BDemo%20Course');
|
||||
expect(axiosMock.history.get[0].url).toContain('block-v1%3AedX%2BDemo%20X%2BDemo%20Course%2Btype%40sequential%2Bblock%40test%20sequence');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -90,14 +90,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([401, 403, 404])(
|
||||
@@ -137,14 +137,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it.each([401, 403, 404])(
|
||||
@@ -185,14 +185,14 @@ describe('Data layer integration tests', () => {
|
||||
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.courseStatus).toEqual('loaded');
|
||||
expect(state).toMatchSnapshot({
|
||||
expect(state).toEqual(expect.objectContaining({
|
||||
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
|
||||
// to keep track of conversations. This causes snapshots to fail, because this UUID
|
||||
// is generated on each run of the snapshot. Instead, we use an asymmetric matcher here.
|
||||
// to keep track of conversations. This UUID is generated on each run.
|
||||
// Instead, we use an asymmetric matcher here.
|
||||
learningAssistant: expect.objectContaining({
|
||||
conversationId: expect.any(String),
|
||||
}),
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('Should handle the url including a targetUserId', async () => {
|
||||
@@ -297,178 +297,4 @@ describe('Data layer integration tests', () => {
|
||||
expect(enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Test fetchExamAttemptsData', () => {
|
||||
const sequenceIds = [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@abcde',
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Mock individual exam endpoints with different responses
|
||||
sequenceIds.forEach((sequenceId, index) => {
|
||||
// Handle both LMS and EXAMS service URL patterns
|
||||
const lmsExamUrl = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceId)}.*`);
|
||||
const examsServiceUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||
|
||||
let attemptStatus = 'ready_to_start';
|
||||
if (index === 0) {
|
||||
attemptStatus = 'created';
|
||||
} else if (index === 1) {
|
||||
attemptStatus = 'submitted';
|
||||
}
|
||||
|
||||
const mockExamData = {
|
||||
exam: {
|
||||
id: index + 1,
|
||||
course_id: courseId,
|
||||
content_id: sequenceId,
|
||||
exam_name: `Test Exam ${index + 1}`,
|
||||
attempt_status: attemptStatus,
|
||||
time_remaining_seconds: 3600,
|
||||
},
|
||||
};
|
||||
|
||||
// Mock both URL patterns
|
||||
axiosMock.onGet(lmsExamUrl).reply(200, mockExamData);
|
||||
axiosMock.onGet(examsServiceUrl).reply(200, mockExamData);
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch exam data for all sequence IDs and dispatch setExamsData', async () => {
|
||||
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
// Verify the examsData was set in the store
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
expect(state.courseHome.examsData).toEqual([
|
||||
{
|
||||
id: 1,
|
||||
courseId,
|
||||
contentId: sequenceIds[0],
|
||||
examName: 'Test Exam 1',
|
||||
attemptStatus: 'created',
|
||||
timeRemainingSeconds: 3600,
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
courseId,
|
||||
contentId: sequenceIds[1],
|
||||
examName: 'Test Exam 2',
|
||||
attemptStatus: 'submitted',
|
||||
timeRemainingSeconds: 3600,
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
courseId,
|
||||
contentId: sequenceIds[2],
|
||||
examName: 'Test Exam 3',
|
||||
attemptStatus: 'ready_to_start',
|
||||
timeRemainingSeconds: 3600,
|
||||
},
|
||||
]);
|
||||
|
||||
// Verify all API calls were made
|
||||
expect(axiosMock.history.get).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle 404 responses and include empty objects in results', async () => {
|
||||
// Override one endpoint to return 404 for both URL patterns
|
||||
const examUrl404LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||
const examUrl404Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||
axiosMock.onGet(examUrl404LMS).reply(404);
|
||||
axiosMock.onGet(examUrl404Exams).reply(404);
|
||||
|
||||
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
// Verify the examsData includes empty object for 404 response
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
expect(state.courseHome.examsData[1]).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle API errors and log them while continuing with other requests', async () => {
|
||||
// Override one endpoint to return 500 error for both URL patterns
|
||||
const examUrl500LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||
const examUrl500Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||
axiosMock.onGet(examUrl500LMS).reply(500, { error: 'Server Error' });
|
||||
axiosMock.onGet(examUrl500Exams).reply(500, { error: 'Server Error' });
|
||||
|
||||
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
// Verify error was logged for the failed request
|
||||
expect(loggingService.logError).toHaveBeenCalled();
|
||||
|
||||
// Verify the examsData still includes results for successful requests
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
// First item should be the error result (just empty object for API errors)
|
||||
expect(state.courseHome.examsData[0]).toEqual({});
|
||||
});
|
||||
|
||||
it('should handle empty sequence IDs array', async () => {
|
||||
await executeThunk(thunks.fetchExamAttemptsData(courseId, []), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
expect(state.courseHome.examsData).toEqual([]);
|
||||
expect(axiosMock.history.get).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should handle mixed success and error responses', async () => {
|
||||
// Setup mixed responses
|
||||
const examUrl1LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[0])}.*`);
|
||||
const examUrl1Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[0])}.*`);
|
||||
const examUrl2LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[1])}.*`);
|
||||
const examUrl2Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[1])}.*`);
|
||||
const examUrl3LMS = new RegExp(`.*edx_proctoring/v1/proctored_exam/attempt/course_id/${encodeURIComponent(courseId)}.*content_id=${encodeURIComponent(sequenceIds[2])}.*`);
|
||||
const examUrl3Exams = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceIds[2])}.*`);
|
||||
|
||||
axiosMock.onGet(examUrl1LMS).reply(200, {
|
||||
exam: {
|
||||
id: 1,
|
||||
exam_name: 'Success Exam',
|
||||
course_id: courseId,
|
||||
content_id: sequenceIds[0],
|
||||
attempt_status: 'created',
|
||||
time_remaining_seconds: 3600,
|
||||
},
|
||||
});
|
||||
axiosMock.onGet(examUrl1Exams).reply(200, {
|
||||
exam: {
|
||||
id: 1,
|
||||
exam_name: 'Success Exam',
|
||||
course_id: courseId,
|
||||
content_id: sequenceIds[0],
|
||||
attempt_status: 'created',
|
||||
time_remaining_seconds: 3600,
|
||||
},
|
||||
});
|
||||
axiosMock.onGet(examUrl2LMS).reply(404);
|
||||
axiosMock.onGet(examUrl2Exams).reply(404);
|
||||
axiosMock.onGet(examUrl3LMS).reply(500, { error: 'Server Error' });
|
||||
axiosMock.onGet(examUrl3Exams).reply(500, { error: 'Server Error' });
|
||||
|
||||
await executeThunk(thunks.fetchExamAttemptsData(courseId, sequenceIds), store.dispatch);
|
||||
|
||||
const state = store.getState();
|
||||
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
expect(state.courseHome.examsData[0]).toMatchObject({
|
||||
id: 1,
|
||||
examName: 'Success Exam',
|
||||
courseId,
|
||||
contentId: sequenceIds[0],
|
||||
});
|
||||
expect(state.courseHome.examsData[1]).toEqual({});
|
||||
expect(state.courseHome.examsData[2]).toEqual({});
|
||||
|
||||
// Verify error was logged for the 500 error (may be called more than once due to multiple URL patterns)
|
||||
expect(loggingService.logError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -18,7 +18,6 @@ const slice = createSlice({
|
||||
toastBodyLink: null,
|
||||
toastHeader: '',
|
||||
showSearch: false,
|
||||
examsData: null,
|
||||
},
|
||||
reducers: {
|
||||
fetchProctoringInfoResolved: (state) => {
|
||||
@@ -54,9 +53,6 @@ const slice = createSlice({
|
||||
setShowSearch: (state, { payload }) => {
|
||||
state.showSearch = payload;
|
||||
},
|
||||
setExamsData: (state, { payload }) => {
|
||||
state.examsData = payload;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -68,7 +64,6 @@ export const {
|
||||
fetchTabSuccess,
|
||||
setCallToActionToast,
|
||||
setShowSearch,
|
||||
setExamsData,
|
||||
} = slice.actions;
|
||||
|
||||
export const {
|
||||
|
||||
@@ -1,145 +0,0 @@
|
||||
import { reducer, setExamsData } from './slice';
|
||||
|
||||
describe('course home data slice', () => {
|
||||
describe('setExamsData reducer', () => {
|
||||
it('should set examsData in state', () => {
|
||||
const initialState = {
|
||||
courseStatus: 'loading',
|
||||
courseId: null,
|
||||
metadataModel: 'courseHomeCourseMetadata',
|
||||
proctoringPanelStatus: 'loading',
|
||||
tabFetchStates: {},
|
||||
toastBodyText: '',
|
||||
toastBodyLink: null,
|
||||
toastHeader: '',
|
||||
showSearch: false,
|
||||
examsData: null,
|
||||
};
|
||||
|
||||
const mockExamsData = [
|
||||
{
|
||||
id: 1,
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
examName: 'Midterm Exam',
|
||||
attemptStatus: 'created',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
examName: 'Final Exam',
|
||||
attemptStatus: 'submitted',
|
||||
},
|
||||
];
|
||||
|
||||
const action = setExamsData(mockExamsData);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
expect(newState.examsData).toEqual(mockExamsData);
|
||||
expect(newState).toEqual({
|
||||
...initialState,
|
||||
examsData: mockExamsData,
|
||||
});
|
||||
});
|
||||
|
||||
it('should update examsData when state already has data', () => {
|
||||
const initialState = {
|
||||
courseStatus: 'loaded',
|
||||
courseId: 'test-course',
|
||||
metadataModel: 'courseHomeCourseMetadata',
|
||||
proctoringPanelStatus: 'loading',
|
||||
tabFetchStates: {},
|
||||
toastBodyText: '',
|
||||
toastBodyLink: null,
|
||||
toastHeader: '',
|
||||
showSearch: false,
|
||||
examsData: [{ id: 1, examName: 'Old Exam' }],
|
||||
};
|
||||
|
||||
const newExamsData = [
|
||||
{
|
||||
id: 2,
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
examName: 'New Exam',
|
||||
attemptStatus: 'ready_to_start',
|
||||
},
|
||||
];
|
||||
|
||||
const action = setExamsData(newExamsData);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
expect(newState.examsData).toEqual(newExamsData);
|
||||
expect(newState.examsData).not.toEqual(initialState.examsData);
|
||||
});
|
||||
|
||||
it('should set examsData to empty array', () => {
|
||||
const initialState = {
|
||||
courseStatus: 'loaded',
|
||||
courseId: 'test-course',
|
||||
metadataModel: 'courseHomeCourseMetadata',
|
||||
proctoringPanelStatus: 'loading',
|
||||
tabFetchStates: {},
|
||||
toastBodyText: '',
|
||||
toastBodyLink: null,
|
||||
toastHeader: '',
|
||||
showSearch: false,
|
||||
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||
};
|
||||
|
||||
const action = setExamsData([]);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
expect(newState.examsData).toEqual([]);
|
||||
});
|
||||
|
||||
it('should set examsData to null', () => {
|
||||
const initialState = {
|
||||
courseStatus: 'loaded',
|
||||
courseId: 'test-course',
|
||||
metadataModel: 'courseHomeCourseMetadata',
|
||||
proctoringPanelStatus: 'loading',
|
||||
tabFetchStates: {},
|
||||
toastBodyText: '',
|
||||
toastBodyLink: null,
|
||||
toastHeader: '',
|
||||
showSearch: false,
|
||||
examsData: [{ id: 1, examName: 'Some Exam' }],
|
||||
};
|
||||
|
||||
const action = setExamsData(null);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
expect(newState.examsData).toBeNull();
|
||||
});
|
||||
|
||||
it('should not affect other state properties when setting examsData', () => {
|
||||
const initialState = {
|
||||
courseStatus: 'loaded',
|
||||
courseId: 'test-course-id',
|
||||
metadataModel: 'courseHomeCourseMetadata',
|
||||
proctoringPanelStatus: 'complete',
|
||||
tabFetchStates: { progress: 'loaded' },
|
||||
toastBodyText: 'Toast message',
|
||||
toastBodyLink: 'http://example.com',
|
||||
toastHeader: 'Toast Header',
|
||||
showSearch: true,
|
||||
examsData: null,
|
||||
};
|
||||
|
||||
const mockExamsData = [{ id: 1, examName: 'Test Exam' }];
|
||||
const action = setExamsData(mockExamsData);
|
||||
const newState = reducer(initialState, action);
|
||||
|
||||
// Verify that only examsData changed
|
||||
expect(newState).toEqual({
|
||||
...initialState,
|
||||
examsData: mockExamsData,
|
||||
});
|
||||
|
||||
// Verify other properties remain unchanged
|
||||
expect(newState.courseStatus).toBe(initialState.courseStatus);
|
||||
expect(newState.courseId).toBe(initialState.courseId);
|
||||
expect(newState.showSearch).toBe(initialState.showSearch);
|
||||
expect(newState.toastBodyText).toBe(initialState.toastBodyText);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4,7 +4,6 @@ import {
|
||||
executePostFromPostEvent,
|
||||
getCourseHomeCourseMetadata,
|
||||
getDatesTabData,
|
||||
getExamsData,
|
||||
getOutlineTabData,
|
||||
getProgressTabData,
|
||||
postCourseDeadlines,
|
||||
@@ -27,7 +26,6 @@ import {
|
||||
fetchTabRequest,
|
||||
fetchTabSuccess,
|
||||
setCallToActionToast,
|
||||
setExamsData,
|
||||
} from './slice';
|
||||
|
||||
import mapSearchResponse from '../courseware-search/map-search-response';
|
||||
@@ -225,19 +223,3 @@ export function searchCourseContent(courseId, searchKeyword) {
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchExamAttemptsData(courseId, sequenceIds) {
|
||||
return async (dispatch) => {
|
||||
const results = await Promise.all(sequenceIds.map(async (sequenceId) => {
|
||||
try {
|
||||
const response = await getExamsData(courseId, sequenceId);
|
||||
return response.exam || {};
|
||||
} catch (e) {
|
||||
logError(e);
|
||||
return {};
|
||||
}
|
||||
}));
|
||||
|
||||
dispatch(setExamsData(results));
|
||||
};
|
||||
}
|
||||
|
||||
@@ -171,7 +171,6 @@ const OutlineTab = () => {
|
||||
</div>
|
||||
{rootCourseId && (
|
||||
<div className="col col-12 col-md-4">
|
||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||
<ProctoringInfoPanel />
|
||||
{ /** Defer showing the goal widget until the ProctoringInfoPanel has resolved or has been determined as
|
||||
disabled to avoid components bouncing around too much as screen is rendered */ }
|
||||
@@ -182,6 +181,7 @@ const OutlineTab = () => {
|
||||
/>
|
||||
)}
|
||||
<CourseTools />
|
||||
<CourseOutlineTabNotificationsSlot courseId={courseId} />
|
||||
<CourseDates />
|
||||
<CourseHandouts />
|
||||
</div>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useDispatch, useSelector } from 'react-redux';
|
||||
import camelCase from 'lodash.camelcase';
|
||||
|
||||
import { useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { getExternalLinkUrl } from '@edx/frontend-platform';
|
||||
import { Button } from '@openedx/paragon';
|
||||
|
||||
import messages from '../messages';
|
||||
@@ -207,7 +208,7 @@ const ProctoringInfoPanel = () => {
|
||||
{isSubmissionRequired(readableStatus) && (
|
||||
onboardingExamButton
|
||||
)}
|
||||
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
|
||||
<Button variant="outline-primary" block href={getExternalLinkUrl('https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams')}>
|
||||
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import React from 'react';
|
||||
import { useWindowSize } from '@openedx/paragon';
|
||||
import { useContextId } from '../../data/hooks';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
import ProgressTabCertificateStatusSidePanelSlot from '../../plugin-slots/ProgressTabCertificateStatusSidePanelSlot';
|
||||
|
||||
import CourseCompletion from './course-completion/CourseCompletion';
|
||||
@@ -11,17 +10,11 @@ import ProgressTabCertificateStatusMainBodySlot from '../../plugin-slots/Progres
|
||||
import ProgressTabCourseGradeSlot from '../../plugin-slots/ProgressTabCourseGradeSlot';
|
||||
import ProgressTabGradeBreakdownSlot from '../../plugin-slots/ProgressTabGradeBreakdownSlot';
|
||||
import ProgressTabRelatedLinksSlot from '../../plugin-slots/ProgressTabRelatedLinksSlot';
|
||||
import { useGetExamsData } from './hooks';
|
||||
import { useModel } from '../../generic/model-store';
|
||||
|
||||
const ProgressTab = () => {
|
||||
const courseId = useContextId();
|
||||
const { disableProgressGraph, sectionScores } = useModel('progress', courseId);
|
||||
|
||||
const sequenceIds = useMemo(() => (
|
||||
sectionScores.flatMap((section) => (section.subsections)).map((subsection) => subsection.blockKey)
|
||||
), [sectionScores]);
|
||||
|
||||
useGetExamsData(courseId, sequenceIds);
|
||||
const { disableProgressGraph } = useModel('progress', courseId);
|
||||
|
||||
const windowWidth = useWindowSize().width;
|
||||
if (windowWidth === undefined) {
|
||||
|
||||
@@ -661,133 +661,143 @@ describe('Progress Tab', () => {
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render Grade Summary when assignment type grade summary is not populated', async () => {
|
||||
it('does not render Grade Summary when assignment policies are not populated', async () => {
|
||||
setTabData({
|
||||
assignment_type_grade_summary: [],
|
||||
grading_policy: {
|
||||
assignment_policies: [],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
section_scores: [],
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows lock icon when all subsections of assignment type are hidden', async () => {
|
||||
it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 2,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 0% 0%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of droppable assignments is less than total number of assignments', async () => {
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of droppable assignments is zero', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 100% 50% 50%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of total assignments is less than the number of assignments created', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 1,
|
||||
num_total: 1, // two assignments created in the factory, but 1 is expected per Studio settings
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 100% 100% 100%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 5, // two assignments created in the factory, but 5 are expected per Studio settings
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
});
|
||||
await fetchAndRender();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 100% 20% 20%' })).toBeInTheDocument();
|
||||
});
|
||||
it('calculates weighted grades correctly', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 1,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 0.5,
|
||||
},
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 1,
|
||||
short_label: 'Final',
|
||||
type: 'Final Exam',
|
||||
weight: 1,
|
||||
short_label: 'Ex',
|
||||
type: 'Exam',
|
||||
weight: 0.5,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Final Exam',
|
||||
weight: 0.4,
|
||||
average_grade: 0.0,
|
||||
weighted_grade: 0.0,
|
||||
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||
has_hidden_contribution: 'all',
|
||||
short_label: 'Final',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
// Should show lock icon for grade and weighted grade
|
||||
expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
average_grade: 0.25,
|
||||
weighted_grade: 0.25,
|
||||
last_grade_publish_date: '2025-10-15T14:17:04.368903Z',
|
||||
has_hidden_contribution: 'some',
|
||||
short_label: 'HW',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
await fetchAndRender();
|
||||
// Should show percent + hidden scores for grade and weighted grade
|
||||
const hiddenScoresCells = screen.getAllByText(/% \+ Hidden Scores/);
|
||||
expect(hiddenScoresCells).toHaveLength(2);
|
||||
// Only correct visible scores should be shown (from subsection2)
|
||||
// The correct visible score is 1/4 = 0.25 -> 25%
|
||||
expect(hiddenScoresCells[0]).toHaveTextContent('25% + Hidden Scores');
|
||||
expect(hiddenScoresCells[1]).toHaveTextContent('25% + Hidden Scores');
|
||||
});
|
||||
|
||||
it('displays a warning message with the latest due date when not all assignment scores are included in the total grade', async () => {
|
||||
setTabData({
|
||||
grading_policy: {
|
||||
assignment_policies: [
|
||||
{
|
||||
num_droppable: 0,
|
||||
num_total: 2,
|
||||
short_label: 'HW',
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
},
|
||||
],
|
||||
grade_range: {
|
||||
pass: 0.75,
|
||||
},
|
||||
},
|
||||
assignment_type_grade_summary: [
|
||||
{
|
||||
type: 'Homework',
|
||||
weight: 1,
|
||||
average_grade: 1,
|
||||
weighted_grade: 1,
|
||||
last_grade_publish_date: tomorrow.toISOString(),
|
||||
has_hidden_contribution: 'none',
|
||||
short_label: 'HW',
|
||||
num_droppable: 0,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
const formattedDateTime = new Intl.DateTimeFormat('en', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
}).format(tomorrow);
|
||||
|
||||
expect(
|
||||
screen.getByText(
|
||||
`Some assignment scores are not yet included in your total grade. These grades will be released by ${formattedDateTime}.`,
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.getByText('Grade summary')).toBeInTheDocument();
|
||||
// The row is comprised of "{Assignment type} {footnote - optional} {weight} {grade} {weighted grade}"
|
||||
expect(screen.getByRole('row', { name: 'Homework 1 50% 100% 50%' })).toBeInTheDocument();
|
||||
expect(screen.getByRole('row', { name: 'Exam 50% 0% 0%' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders override notice', async () => {
|
||||
@@ -1490,287 +1500,4 @@ describe('Progress Tab', () => {
|
||||
expect(screen.getByText('Course progress for otherstudent')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe('Exam data fetching integration', () => {
|
||||
const mockSectionScores = [
|
||||
{
|
||||
display_name: 'Section 1',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Exam',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||
display_name: 'Midterm Exam',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: true,
|
||||
percent_graded: 0.8,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: '/mock-url',
|
||||
},
|
||||
{
|
||||
assignment_type: 'Homework',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@homework1',
|
||||
display_name: 'Homework 1',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: true,
|
||||
percent_graded: 0.9,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: '/mock-url',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display_name: 'Section 2',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Exam',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||
display_name: 'Final Exam',
|
||||
learner_has_access: true,
|
||||
has_graded_assignment: true,
|
||||
percent_graded: 0.85,
|
||||
show_correctness: 'always',
|
||||
show_grades: true,
|
||||
url: '/mock-url',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
// Reset any existing handlers to avoid conflicts
|
||||
axiosMock.reset();
|
||||
|
||||
// Re-add the base mocks that other tests expect
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||
|
||||
// Mock exam data endpoints using specific GET handlers
|
||||
axiosMock.onGet(/.*exam1.*/).reply(200, {
|
||||
exam: {
|
||||
id: 1,
|
||||
course_id: courseId,
|
||||
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||
exam_name: 'Midterm Exam',
|
||||
attempt_status: 'submitted',
|
||||
time_remaining_seconds: 0,
|
||||
},
|
||||
});
|
||||
|
||||
axiosMock.onGet(/.*homework1.*/).reply(404);
|
||||
|
||||
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||
exam: {
|
||||
id: 2,
|
||||
course_id: courseId,
|
||||
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||
exam_name: 'Final Exam',
|
||||
attempt_status: 'ready_to_start',
|
||||
time_remaining_seconds: 7200,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should fetch exam data for all subsections when ProgressTab renders', async () => {
|
||||
setTabData({ section_scores: mockSectionScores });
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify exam API calls were made for all subsections
|
||||
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||
|
||||
// Verify the exam data is in the Redux store
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
|
||||
// Check the exam data structure
|
||||
expect(state.courseHome.examsData[0]).toEqual({
|
||||
id: 1,
|
||||
courseId,
|
||||
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@exam1',
|
||||
examName: 'Midterm Exam',
|
||||
attemptStatus: 'submitted',
|
||||
timeRemainingSeconds: 0,
|
||||
});
|
||||
|
||||
expect(state.courseHome.examsData[1]).toEqual({}); // 404 response for homework
|
||||
|
||||
expect(state.courseHome.examsData[2]).toEqual({
|
||||
id: 2,
|
||||
courseId,
|
||||
contentId: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||
examName: 'Final Exam',
|
||||
attemptStatus: 'ready_to_start',
|
||||
timeRemainingSeconds: 7200,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle empty section scores gracefully', async () => {
|
||||
setTabData({ section_scores: [] });
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify no exam API calls were made
|
||||
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(0);
|
||||
|
||||
// Verify empty exam data in Redux store
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.examsData).toEqual([]);
|
||||
});
|
||||
|
||||
it('should re-fetch exam data when section scores change', async () => {
|
||||
// Initial render with limited section scores
|
||||
setTabData({
|
||||
section_scores: [mockSectionScores[0]], // Only first section
|
||||
});
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify initial API calls (2 subsections in first section)
|
||||
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(2);
|
||||
|
||||
// Clear axios history to track new calls
|
||||
axiosMock.resetHistory();
|
||||
|
||||
// Update with full section scores and re-render
|
||||
setTabData({ section_scores: mockSectionScores });
|
||||
await executeThunk(thunks.fetchProgressTab(courseId), store.dispatch);
|
||||
|
||||
// Verify additional API calls for all subsections
|
||||
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should handle exam API errors gracefully without breaking ProgressTab', async () => {
|
||||
// Clear existing mocks and setup specific error scenario
|
||||
axiosMock.reset();
|
||||
|
||||
// Re-add base mocks
|
||||
axiosMock.onGet(courseMetadataUrl).reply(200, defaultMetadata);
|
||||
axiosMock.onGet(progressUrl).reply(200, defaultTabData);
|
||||
axiosMock.onGet(masqueradeUrl).reply(200, { success: true });
|
||||
|
||||
// Mock first exam to return 500 error
|
||||
axiosMock.onGet(/.*exam1.*/).reply(500, { error: 'Server Error' });
|
||||
|
||||
// Mock other exams to succeed
|
||||
axiosMock.onGet(/.*homework1.*/).reply(404, { customAttributes: { httpErrorStatus: 404 } });
|
||||
axiosMock.onGet(/.*final_exam.*/).reply(200, {
|
||||
exam: {
|
||||
id: 2,
|
||||
course_id: courseId,
|
||||
content_id: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@final_exam',
|
||||
exam_name: 'Final Exam',
|
||||
attempt_status: 'ready_to_start',
|
||||
time_remaining_seconds: 7200,
|
||||
},
|
||||
});
|
||||
|
||||
setTabData({ section_scores: mockSectionScores });
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify ProgressTab still renders successfully despite API error
|
||||
expect(screen.getByText('Grades')).toBeInTheDocument();
|
||||
|
||||
// Verify the exam data includes error placeholder for failed request
|
||||
const state = store.getState();
|
||||
expect(state.courseHome.examsData).toHaveLength(3);
|
||||
expect(state.courseHome.examsData[0]).toEqual({}); // Failed request returns empty object
|
||||
});
|
||||
|
||||
it('should use EXAMS_BASE_URL when configured for exam API calls', async () => {
|
||||
// Configure EXAMS_BASE_URL
|
||||
const originalConfig = getConfig();
|
||||
setConfig({
|
||||
...originalConfig,
|
||||
EXAMS_BASE_URL: 'http://localhost:18740',
|
||||
});
|
||||
|
||||
// Override mock to use new base URL
|
||||
const examUrlWithExamsBase = /http:\/\/localhost:18740\/api\/v1\/student\/exam\/attempt\/course_id.*/;
|
||||
axiosMock.onGet(examUrlWithExamsBase).reply(200, {
|
||||
exam: {
|
||||
id: 1,
|
||||
course_id: courseId,
|
||||
exam_name: 'Test Exam',
|
||||
attempt_status: 'created',
|
||||
},
|
||||
});
|
||||
|
||||
setTabData({ section_scores: [mockSectionScores[0]] });
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify API calls use EXAMS_BASE_URL
|
||||
const examApiCalls = axiosMock.history.get.filter(req => req.url.includes('localhost:18740'));
|
||||
expect(examApiCalls.length).toBeGreaterThan(0);
|
||||
|
||||
// Restore original config
|
||||
setConfig(originalConfig);
|
||||
});
|
||||
|
||||
it('should extract sequence IDs correctly from nested section scores structure', async () => {
|
||||
const complexSectionScores = [
|
||||
{
|
||||
display_name: 'Introduction',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Lecture',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||
display_name: 'Course Introduction',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
display_name: 'Assessments',
|
||||
subsections: [
|
||||
{
|
||||
assignment_type: 'Exam',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||
display_name: 'Quiz 1',
|
||||
},
|
||||
{
|
||||
assignment_type: 'Exam',
|
||||
block_key: 'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||
display_name: 'Quiz 2',
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// Mock all the expected sequence IDs
|
||||
const expectedSequenceIds = [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@intro',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz1',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@quiz2',
|
||||
];
|
||||
|
||||
expectedSequenceIds.forEach((sequenceId, index) => {
|
||||
const examUrl = new RegExp(`.*/api/v1/student/exam/attempt/course_id/${encodeURIComponent(courseId)}/content_id/${encodeURIComponent(sequenceId)}.*`);
|
||||
axiosMock.onGet(examUrl).reply(index === 0 ? 404 : 200, {
|
||||
exam: {
|
||||
id: index,
|
||||
course_id: courseId,
|
||||
content_id: sequenceId,
|
||||
exam_name: `Test ${index}`,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
setTabData({ section_scores: complexSectionScores });
|
||||
|
||||
await fetchAndRender();
|
||||
|
||||
// Verify API calls were made for all extracted sequence IDs
|
||||
expect(axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'))).toHaveLength(3);
|
||||
|
||||
// Verify correct sequence IDs were used in API calls
|
||||
const apiCalls = axiosMock.history.get.filter(req => req.url.includes('/api/v1/student/exam/attempt/'));
|
||||
expectedSequenceIds.forEach(sequenceId => {
|
||||
expect(apiCalls.some(call => call.url.includes(encodeURIComponent(sequenceId)))).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -187,7 +187,8 @@ const CertificateStatus = () => {
|
||||
// regardless of passing or nonpassing status
|
||||
if (!canViewCertificate) {
|
||||
certCase = 'notAvailable';
|
||||
endDate = intl.formatDate(end, {
|
||||
// use the certificate_available_date if it is available, otherwise use the end date of the course
|
||||
endDate = intl.formatDate((certificateAvailableDate || end), {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
|
||||
@@ -8,57 +8,26 @@ import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
import GradeRangeTooltip from './GradeRangeTooltip';
|
||||
import messages from '../messages';
|
||||
import { getLatestDueDateInFuture } from '../../utils';
|
||||
|
||||
const ResponsiveText = ({
|
||||
wideScreen, children, hasLetterGrades, passingGrade,
|
||||
}) => {
|
||||
const className = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||
const iconSize = wideScreen ? 'h3' : 'h4';
|
||||
|
||||
return (
|
||||
<span className={className}>
|
||||
{children}
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName={iconSize} passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
const NoticeRow = ({
|
||||
wideScreen, icon, bgClass, message,
|
||||
}) => {
|
||||
const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
|
||||
return (
|
||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${bgClass}`}>
|
||||
<div className="col-auto p-0">{icon}</div>
|
||||
<div className="col-11 pl-2 px-0">
|
||||
<span className={textClass}>{message}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const CourseGradeFooter = ({ passingGrade }) => {
|
||||
const intl = useIntl();
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
assignmentTypeGradeSummary,
|
||||
courseGrade: { isPassing, letterGrade },
|
||||
gradingPolicy: { gradeRange },
|
||||
courseGrade: {
|
||||
isPassing,
|
||||
letterGrade,
|
||||
},
|
||||
gradingPolicy: {
|
||||
gradeRange,
|
||||
},
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const latestDueDate = getLatestDueDateInFuture(assignmentTypeGradeSummary);
|
||||
const wideScreen = useWindowSize().width >= breakpoints.medium.minWidth;
|
||||
const hasLetterGrades = Object.keys(gradeRange).length > 1;
|
||||
|
||||
// build footer text
|
||||
const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
|
||||
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
|
||||
|
||||
if (isPassing) {
|
||||
if (hasLetterGrades) {
|
||||
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
|
||||
@@ -78,63 +47,42 @@ const CourseGradeFooter = ({ passingGrade }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const passingIcon = isPassing ? (
|
||||
<Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||
) : (
|
||||
<Icon src={WarningFilled} className="d-inline-flex align-bottom" />
|
||||
);
|
||||
const icon = isPassing ? <Icon src={CheckCircle} className="text-success-300 d-inline-flex align-bottom" />
|
||||
: <Icon src={WarningFilled} className="d-inline-flex align-bottom" />;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<NoticeRow
|
||||
wideScreen={wideScreen}
|
||||
icon={passingIcon}
|
||||
bgClass={isPassing ? 'bg-success-100' : 'bg-warning-100'}
|
||||
message={(
|
||||
<ResponsiveText
|
||||
wideScreen={wideScreen}
|
||||
hasLetterGrades={hasLetterGrades}
|
||||
passingGrade={passingGrade}
|
||||
>
|
||||
<div className={`row w-100 m-0 px-4 py-3 py-md-4 rounded-bottom ${isPassing ? 'bg-success-100' : 'bg-warning-100'}`}>
|
||||
<div className="col-auto p-0">
|
||||
{icon}
|
||||
</div>
|
||||
<div className="col-11 pl-2 px-0">
|
||||
{!wideScreen && (
|
||||
<span className="h5 align-bottom">
|
||||
{footerText}
|
||||
</ResponsiveText>
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName="h4" passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
/>
|
||||
{latestDueDate && (
|
||||
<NoticeRow
|
||||
wideScreen={wideScreen}
|
||||
icon={<Icon src={WarningFilled} className="d-inline-flex align-bottom" />}
|
||||
bgClass="bg-warning-100"
|
||||
message={intl.formatMessage(messages.courseGradeFooterDueDateNotice, {
|
||||
dueDate: intl.formatDate(latestDueDate, {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
timeZoneName: 'short',
|
||||
}),
|
||||
})}
|
||||
/>
|
||||
)}
|
||||
{wideScreen && (
|
||||
<span className="h4 m-0 align-bottom">
|
||||
{footerText}
|
||||
{hasLetterGrades && (
|
||||
<span style={{ whiteSpace: 'nowrap' }}>
|
||||
|
||||
<GradeRangeTooltip iconButtonClassName="h3" passingGrade={passingGrade} />
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
ResponsiveText.propTypes = {
|
||||
wideScreen: PropTypes.bool.isRequired,
|
||||
children: PropTypes.node.isRequired,
|
||||
hasLetterGrades: PropTypes.bool.isRequired,
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
NoticeRow.propTypes = {
|
||||
wideScreen: PropTypes.bool.isRequired,
|
||||
icon: PropTypes.element.isRequired,
|
||||
bgClass: PropTypes.string.isRequired,
|
||||
message: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
CourseGradeFooter.propTypes = {
|
||||
passingGrade: PropTypes.number.isRequired,
|
||||
};
|
||||
|
||||
@@ -13,7 +13,6 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
assignmentTypeGradeSummary,
|
||||
courseGrade: {
|
||||
isPassing,
|
||||
percent,
|
||||
@@ -26,8 +25,6 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none');
|
||||
|
||||
if (isLocaleRtl) {
|
||||
currentGradeDirection = currentGrade < 50 ? '-' : '';
|
||||
}
|
||||
@@ -59,15 +56,6 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
|
||||
>
|
||||
{intl.formatMessage(messages.currentGradeLabel)}
|
||||
</text>
|
||||
<text
|
||||
className="x-small"
|
||||
textAnchor={currentGrade < 50 ? 'start' : 'end'}
|
||||
x={`${Math.min(...[isLocaleRtl ? 100 - currentGrade : currentGrade, 100])}%`}
|
||||
y="35px"
|
||||
style={{ transform: `translateX(${currentGradeDirection}3.4em)` }}
|
||||
>
|
||||
{hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''}
|
||||
</text>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -10,12 +10,14 @@ const GradeSummary = () => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
assignmentTypeGradeSummary,
|
||||
gradingPolicy: {
|
||||
assignmentPolicies,
|
||||
},
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false);
|
||||
|
||||
if (assignmentTypeGradeSummary.length === 0) {
|
||||
if (assignmentPolicies.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import PropTypes from 'prop-types';
|
||||
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { DataTable } from '@openedx/paragon';
|
||||
import { Lock } from '@openedx/paragon/icons';
|
||||
import { useContextId } from '../../../../data/hooks';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
|
||||
@@ -17,7 +16,9 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
assignmentTypeGradeSummary,
|
||||
gradingPolicy: {
|
||||
assignmentPolicies,
|
||||
},
|
||||
gradesFeatureIsFullyLocked,
|
||||
sectionScores,
|
||||
} = useModel('progress', courseId);
|
||||
@@ -54,7 +55,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
return false;
|
||||
};
|
||||
|
||||
const gradeSummaryData = assignmentTypeGradeSummary.map((assignment) => {
|
||||
const gradeSummaryData = assignmentPolicies.map((assignment) => {
|
||||
const {
|
||||
averageGrade,
|
||||
numDroppable,
|
||||
@@ -79,24 +80,13 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
const locked = !gradesFeatureIsFullyLocked && hasNoAccessToAssignmentsOfType(assignmentType);
|
||||
const isLocaleRtl = isRtl(getLocale());
|
||||
|
||||
let weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||
let gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`;
|
||||
|
||||
if (assignment.hasHiddenContribution === 'all') {
|
||||
gradeDisplay = <Lock data-testid="lock-icon" />;
|
||||
weightedGradeDisplay = <Lock data-testid="lock-icon" />;
|
||||
} else if (assignment.hasHiddenContribution === 'some') {
|
||||
gradeDisplay = `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||
weightedGradeDisplay = `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}% + ${intl.formatMessage(messages.hiddenScoreLabel)}`;
|
||||
}
|
||||
|
||||
return {
|
||||
type: {
|
||||
footnoteId, footnoteMarker, type: assignmentType, locked,
|
||||
},
|
||||
weight: { weight: `${(weight * 100).toFixed(0)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
grade: { grade: gradeDisplay, locked },
|
||||
weightedGrade: { weightedGrade: weightedGradeDisplay, locked },
|
||||
grade: { grade: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
|
||||
};
|
||||
});
|
||||
const getAssignmentTypeCell = (value) => (
|
||||
@@ -112,16 +102,6 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<ul className="micro mb-3 pl-3 text-gray-700">
|
||||
<li>
|
||||
<b>{intl.formatMessage(messages.hiddenScoreLabel)}: </b>
|
||||
{intl.formatMessage(messages.hiddenScoreInfoText)}
|
||||
</li>
|
||||
<li>
|
||||
<b><Lock style={{ height: '15px' }} />: </b>
|
||||
{` ${intl.formatMessage(messages.hiddenScoreLockInfoText)}`}
|
||||
</li>
|
||||
</ul>
|
||||
<DataTable
|
||||
data={gradeSummaryData}
|
||||
itemCount={gradeSummaryData.length}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { useContext } from 'react';
|
||||
|
||||
import { getLocale, isRtl, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import {
|
||||
DataTable,
|
||||
DataTableContext,
|
||||
Icon,
|
||||
OverlayTrigger,
|
||||
Stack,
|
||||
@@ -14,6 +17,18 @@ import messages from '../messages';
|
||||
|
||||
const GradeSummaryTableFooter = () => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { data } = useContext(DataTableContext);
|
||||
|
||||
const rawGrade = data.reduce(
|
||||
(grade, currentValue) => {
|
||||
const { weightedGrade } = currentValue.weightedGrade;
|
||||
const percent = weightedGrade.replace(/%/g, '').trim();
|
||||
return grade + parseFloat(percent);
|
||||
},
|
||||
0,
|
||||
).toFixed(2);
|
||||
|
||||
const courseId = useContextId();
|
||||
|
||||
const {
|
||||
@@ -21,16 +36,8 @@ const GradeSummaryTableFooter = () => {
|
||||
isPassing,
|
||||
percent,
|
||||
},
|
||||
finalGrades,
|
||||
} = useModel('progress', courseId);
|
||||
|
||||
const getGradePercent = (grade) => {
|
||||
const percentage = grade * 100;
|
||||
return Number.isInteger(percentage) ? percentage.toFixed(0) : percentage.toFixed(2);
|
||||
};
|
||||
|
||||
const rawGrade = getGradePercent(finalGrades);
|
||||
|
||||
const bgColor = isPassing ? 'bg-success-100' : 'bg-warning-100';
|
||||
const totalGrade = (percent * 100).toFixed(0);
|
||||
|
||||
|
||||
@@ -21,11 +21,6 @@ const messages = defineMessages({
|
||||
defaultMessage: 'Your current grade is {currentGrade}%. A weighted grade of {passingGrade}% is required to pass in this course.',
|
||||
description: 'Alt text for the grade chart bar',
|
||||
},
|
||||
courseGradeFooterDueDateNotice: {
|
||||
id: 'progress.courseGrade.footer.dueDateNotice',
|
||||
defaultMessage: 'Some assignment scores are not yet included in your total grade. These grades will be released by {dueDate}.',
|
||||
description: 'This is shown when there are pending assignments with a due date in the future',
|
||||
},
|
||||
courseGradeFooterGenericPassing: {
|
||||
id: 'progress.courseGrade.footer.generic.passing',
|
||||
defaultMessage: 'You’re currently passing this course',
|
||||
@@ -153,21 +148,6 @@ const messages = defineMessages({
|
||||
+ "Your weighted grade is what's used to determine if you pass the course.",
|
||||
description: 'The content of (tip box) for the grade summary section',
|
||||
},
|
||||
hiddenScoreLabel: {
|
||||
id: 'progress.hiddenScoreLabel',
|
||||
defaultMessage: 'Hidden Scores',
|
||||
description: 'Text to indicate that some scores are hidden',
|
||||
},
|
||||
hiddenScoreInfoText: {
|
||||
id: 'progress.hiddenScoreInfoText',
|
||||
defaultMessage: 'Scores from assignments that count toward your final grade but some are not shown here.',
|
||||
description: 'Information text about hidden score label',
|
||||
},
|
||||
hiddenScoreLockInfoText: {
|
||||
id: 'progress.hiddenScoreLockInfoText',
|
||||
defaultMessage: 'Scores for an assignment type are hidden but still counted toward the course grade.',
|
||||
description: 'Information text about hidden score label when learners have limited access to grades feature',
|
||||
},
|
||||
noAccessToAssignmentType: {
|
||||
id: 'progress.noAcessToAssignmentType',
|
||||
defaultMessage: 'You do not have access to assignments of type {assignmentType}',
|
||||
|
||||
@@ -1,12 +0,0 @@
|
||||
import { useEffect } from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
|
||||
import { fetchExamAttemptsData } from '../data/thunks';
|
||||
|
||||
export function useGetExamsData(courseId, sequenceIds) {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchExamAttemptsData(courseId, sequenceIds));
|
||||
}, [dispatch, courseId, sequenceIds]);
|
||||
}
|
||||
@@ -1,168 +0,0 @@
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useGetExamsData } from './hooks';
|
||||
import { fetchExamAttemptsData } from '../data/thunks';
|
||||
|
||||
// Mock the dependencies
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('../data/thunks', () => ({
|
||||
fetchExamAttemptsData: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('useGetExamsData hook', () => {
|
||||
const mockDispatch = jest.fn();
|
||||
const mockFetchExamAttemptsData = jest.fn();
|
||||
|
||||
beforeEach(() => {
|
||||
useDispatch.mockReturnValue(mockDispatch);
|
||||
fetchExamAttemptsData.mockReturnValue(mockFetchExamAttemptsData);
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
it('should dispatch fetchExamAttemptsData on mount', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceIds = [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||
];
|
||||
|
||||
renderHook(() => useGetExamsData(courseId, sequenceIds));
|
||||
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, sequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
|
||||
it('should re-dispatch when courseId changes', () => {
|
||||
const initialCourseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const newCourseId = 'course-v1:edX+NewCourse+Demo';
|
||||
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||
{
|
||||
initialProps: { courseId: initialCourseId, sequenceIds },
|
||||
},
|
||||
);
|
||||
|
||||
// Verify initial call
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(initialCourseId, sequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
|
||||
// Clear mocks to isolate the re-render call
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-render with new courseId
|
||||
rerender({ courseId: newCourseId, sequenceIds });
|
||||
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(newCourseId, sequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
|
||||
it('should re-dispatch when sequenceIds changes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const initialSequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||
const newSequenceIds = [
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345',
|
||||
'block-v1:edX+DemoX+Demo_Course+type@sequential+block@67890',
|
||||
];
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||
{
|
||||
initialProps: { courseId, sequenceIds: initialSequenceIds },
|
||||
},
|
||||
);
|
||||
|
||||
// Verify initial call
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, initialSequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
|
||||
// Clear mocks to isolate the re-render call
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-render with new sequenceIds
|
||||
rerender({ courseId, sequenceIds: newSequenceIds });
|
||||
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, newSequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
|
||||
it('should not re-dispatch when neither courseId nor sequenceIds changes', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||
{
|
||||
initialProps: { courseId, sequenceIds },
|
||||
},
|
||||
);
|
||||
|
||||
// Verify initial call
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clear mocks to isolate the re-render call
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-render with same props
|
||||
rerender({ courseId, sequenceIds });
|
||||
|
||||
// Should not dispatch again
|
||||
expect(fetchExamAttemptsData).not.toHaveBeenCalled();
|
||||
expect(mockDispatch).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle empty sequenceIds array', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceIds = [];
|
||||
|
||||
renderHook(() => useGetExamsData(courseId, sequenceIds));
|
||||
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, []);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
|
||||
it('should handle null/undefined courseId', () => {
|
||||
const sequenceIds = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||
|
||||
renderHook(() => useGetExamsData(null, sequenceIds));
|
||||
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(null, sequenceIds);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
|
||||
it('should handle sequenceIds reference change but same content', () => {
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
const sequenceIds1 = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345'];
|
||||
const sequenceIds2 = ['block-v1:edX+DemoX+Demo_Course+type@sequential+block@12345']; // Same content, different reference
|
||||
|
||||
const { rerender } = renderHook(
|
||||
({ courseId: cId, sequenceIds: sIds }) => useGetExamsData(cId, sIds),
|
||||
{
|
||||
initialProps: { courseId, sequenceIds: sequenceIds1 },
|
||||
},
|
||||
);
|
||||
|
||||
// Verify initial call
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledTimes(1);
|
||||
expect(mockDispatch).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Clear mocks to isolate the re-render call
|
||||
jest.clearAllMocks();
|
||||
|
||||
// Re-render with different reference but same content
|
||||
rerender({ courseId, sequenceIds: sequenceIds2 });
|
||||
|
||||
// Should dispatch again because the reference changed (useEffect dependency)
|
||||
expect(fetchExamAttemptsData).toHaveBeenCalledWith(courseId, sequenceIds2);
|
||||
expect(mockDispatch).toHaveBeenCalledWith(mockFetchExamAttemptsData);
|
||||
});
|
||||
});
|
||||
@@ -5,15 +5,3 @@ export const showUngradedAssignments = () => (
|
||||
getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === 'true'
|
||||
|| getConfig().SHOW_UNGRADED_ASSIGNMENT_PROGRESS === true
|
||||
);
|
||||
|
||||
export const getLatestDueDateInFuture = (assignmentTypeGradeSummary) => {
|
||||
let latest = null;
|
||||
assignmentTypeGradeSummary.forEach((assignment) => {
|
||||
const assignmentLastGradePublishDate = assignment.lastGradePublishDate;
|
||||
if (assignmentLastGradePublishDate && (!latest || new Date(assignmentLastGradePublishDate) > new Date(latest))
|
||||
&& new Date(assignmentLastGradePublishDate) > new Date()) {
|
||||
latest = assignmentLastGradePublishDate;
|
||||
}
|
||||
});
|
||||
return latest;
|
||||
};
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { getConfig, history } from '@edx/frontend-platform';
|
||||
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
|
||||
import { AppProvider } from '@edx/frontend-platform/react';
|
||||
import { waitForElementToBeRemoved, fireEvent } from '@testing-library/dom';
|
||||
import { waitForElementToBeRemoved } from '@testing-library/dom';
|
||||
import '@testing-library/jest-dom';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
@@ -193,15 +193,13 @@ describe('CoursewareContainer', () => {
|
||||
expect(courseHeader.querySelector('.course-title')).toHaveTextContent(courseHomeMetadata.title);
|
||||
}
|
||||
|
||||
function assertSequenceNavigation(container, expectedUnitCount = 3) {
|
||||
// Ensure we had appropriate sequence navigation buttons. We should only have one unit.
|
||||
function assertNoSequenceNavigation(container) {
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
|
||||
expect(sequenceNavButtons).toHaveLength(expectedUnitCount + 2);
|
||||
expect(sequenceNavButtons).toHaveLength(0);
|
||||
|
||||
expect(sequenceNavButtons[0]).toHaveTextContent('Previous');
|
||||
// Prove this button is rendering an SVG tasks icon, meaning it's a unit/vertical.
|
||||
expect(sequenceNavButtons[1].querySelector('svg')).toHaveClass('fa-tasks');
|
||||
expect(sequenceNavButtons[sequenceNavButtons.length - 1]).toHaveTextContent('Next');
|
||||
expect(container.querySelector('button, a')).not.toHaveTextContent('Previous');
|
||||
expect(container.querySelector('svg.fa-tasks')).toBeNull();
|
||||
expect(container.querySelector('button, a')).not.toHaveTextContent('Next');
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
@@ -224,7 +222,7 @@ describe('CoursewareContainer', () => {
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -247,7 +245,7 @@ describe('CoursewareContainer', () => {
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -274,29 +272,12 @@ describe('CoursewareContainer', () => {
|
||||
setUpMockRequests({ courseBlocks });
|
||||
});
|
||||
|
||||
// describe('when the URL contains a unit ID', () => {
|
||||
// it('should ignore the section ID and redirect based on the unit ID', async () => {
|
||||
// const urlUnit = unitTree[1][1][1];
|
||||
// setUrl(sectionTree[1].id, urlUnit.id);
|
||||
// const container = await loadContainer();
|
||||
// assertLoadedHeader(container);
|
||||
// assertSequenceNavigation(container, 2);
|
||||
// assertLocation(container, sequenceTree[1][1].id, urlUnit.id);
|
||||
// });
|
||||
|
||||
// it('should ignore invalid unit IDs and redirect to the course root', async () => {
|
||||
// setUrl(sectionTree[1].id, 'foobar');
|
||||
// await loadContainer();
|
||||
// expect(global.location.href).toEqual(`http://localhost/course/${courseId}`);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the URL does not contain a unit ID', () => {
|
||||
it('should choose a unit within the section\'s first sequence', async () => {
|
||||
setUrl(sectionTree[1].id);
|
||||
const container = await loadContainer();
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container, 2);
|
||||
assertNoSequenceNavigation(container);
|
||||
assertLocation(container, sequenceTree[1][0].id, unitTree[1][0][0].id);
|
||||
});
|
||||
});
|
||||
@@ -342,27 +323,6 @@ describe('CoursewareContainer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// describe('when the URL only contains a unit ID', () => {
|
||||
// const { courseBlocks, unitTree, sequenceTree } = buildBinaryCourseBlocks(courseId, courseMetadata.name);
|
||||
|
||||
// beforeEach(async () => {
|
||||
// setUpMockRequests({ courseBlocks });
|
||||
// });
|
||||
|
||||
// it('should insert the sequence ID into the URL', async () => {
|
||||
// const unit = unitTree[1][0][1];
|
||||
// history.push(`/course/${courseId}/${unit.id}`);
|
||||
// const container = await loadContainer();
|
||||
|
||||
// assertLoadedHeader(container);
|
||||
// assertSequenceNavigation(container, 2);
|
||||
// const expectedSequenceId = sequenceTree[1][0].id;
|
||||
// const expectedUrl = `http://localhost/course/${courseId}/${expectedSequenceId}/${unit.id}`;
|
||||
// expect(global.location.href).toEqual(expectedUrl);
|
||||
// expect(container.querySelector('.fake-unit')).toHaveTextContent(unit.id);
|
||||
// });
|
||||
// });
|
||||
|
||||
describe('when the URL contains a course ID and sequence ID', () => {
|
||||
const sequenceBlock = defaultSequenceBlock;
|
||||
const unitBlocks = defaultUnitBlocks;
|
||||
@@ -372,7 +332,7 @@ describe('CoursewareContainer', () => {
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -391,7 +351,7 @@ describe('CoursewareContainer', () => {
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
@@ -408,44 +368,24 @@ describe('CoursewareContainer', () => {
|
||||
const container = await loadContainer();
|
||||
|
||||
assertLoadedHeader(container);
|
||||
assertSequenceNavigation(container);
|
||||
assertNoSequenceNavigation(container);
|
||||
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent('Unit Contents');
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(courseId);
|
||||
expect(container.querySelector('.fake-unit')).toHaveTextContent(unitBlocks[2].id);
|
||||
});
|
||||
|
||||
it('should navigate between units and check block completion', async () => {
|
||||
axiosMock.onPost(`${courseId}/xblock/${sequenceBlock.id}/handler/get_completion`).reply(200, {
|
||||
complete: true,
|
||||
});
|
||||
it('should render the sequence_navigation plugin slot correctly', async () => {
|
||||
axiosMock
|
||||
.onPost(`${courseId}/xblock/${sequenceBlock.id}/handler/get_completion`)
|
||||
.reply(200, { complete: true });
|
||||
|
||||
history.push(`/course/${courseId}/${sequenceBlock.id}/${unitBlocks[0].id}`);
|
||||
const container = await loadContainer();
|
||||
await loadContainer();
|
||||
|
||||
const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
|
||||
const sequenceNextButton = sequenceNavButtons[4];
|
||||
expect(sequenceNextButton).toHaveTextContent('Next');
|
||||
fireEvent.click(sequenceNextButton);
|
||||
|
||||
expect(global.location.href).toEqual(`http://localhost/course/${courseId}/${sequenceBlock.id}/${unitBlocks[1].id}`);
|
||||
expect(screen.getByTestId('org.openedx.frontend.learning.sequence_navigation.v1')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
// describe('when the current sequence is an exam', () => {
|
||||
// const { location } = window;
|
||||
|
||||
// beforeEach(() => {
|
||||
// delete window.location;
|
||||
// window.location = {
|
||||
// assign: jest.fn(),
|
||||
// };
|
||||
// });
|
||||
|
||||
// afterEach(() => {
|
||||
// window.location = location;
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
||||
describe('when receiving a course_access error_code', () => {
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Helmet } from 'react-helmet';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { useLocation, useNavigate } from 'react-router-dom';
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
|
||||
import { AlertList } from '@src/generic/user-messages';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { getCoursewareOutlineSidebarSettings } from '../data/selectors';
|
||||
import { LearnerToolsSlot } from '../../plugin-slots/LearnerToolsSlot';
|
||||
import Chat from './chat/Chat';
|
||||
import SidebarProvider from './sidebar/SidebarContextProvider';
|
||||
import NewSidebarProvider from './new-sidebar/SidebarContextProvider';
|
||||
import { NotificationsDiscussionsSidebarTriggerSlot } from '../../plugin-slots/NotificationsDiscussionsSidebarTriggerSlot';
|
||||
@@ -37,8 +36,6 @@ const Course = ({
|
||||
} = useModel('courseHomeMeta', courseId);
|
||||
const sequence = useModel('sequences', sequenceId);
|
||||
const section = useModel('sections', sequence ? sequence.sectionId : null);
|
||||
const { enableNavigationSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const navigationDisabled = enableNavigationSidebar || (sequence?.navigationDisabled ?? false);
|
||||
const navigate = useNavigate();
|
||||
const { pathname } = useLocation();
|
||||
|
||||
@@ -62,7 +59,7 @@ const Course = ({
|
||||
const [weeklyGoalCelebrationOpen, setWeeklyGoalCelebrationOpen] = useState(
|
||||
celebrations && !celebrations.streakLengthToCelebrate && celebrations.weeklyGoal,
|
||||
);
|
||||
const shouldDisplayLearnerTools = windowWidth >= breakpoints.medium.minWidth;
|
||||
const shouldDisplayChat = windowWidth >= breakpoints.medium.minWidth;
|
||||
const daysPerWeek = course?.courseGoals?.selectedGoal?.daysPerWeek;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -84,24 +81,24 @@ const Course = ({
|
||||
<title>{`${pageTitleBreadCrumbs.join(' | ')} | ${getConfig().SITE_NAME}`}</title>
|
||||
</Helmet>
|
||||
<div className="position-relative d-flex align-items-xl-center mb-4 mt-1 flex-column flex-xl-row">
|
||||
{navigationDisabled || (
|
||||
<>
|
||||
<CourseBreadcrumbsSlot
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{shouldDisplayLearnerTools && (
|
||||
<LearnerToolsSlot
|
||||
enrollmentMode={course.enrollmentMode}
|
||||
isStaff={isStaff}
|
||||
courseId={courseId}
|
||||
unitId={unitId}
|
||||
/>
|
||||
<CourseBreadcrumbsSlot
|
||||
courseId={courseId}
|
||||
sectionId={section ? section.id : null}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
{shouldDisplayChat && (
|
||||
<>
|
||||
<Chat
|
||||
enabled={course.learningAssistantEnabled}
|
||||
enrollmentMode={course.enrollmentMode}
|
||||
isStaff={isStaff}
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={course.showCalculator || course.notes.enabled}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
<div className="w-100 d-flex align-items-center">
|
||||
<CourseOutlineMobileSidebarTriggerSlot />
|
||||
|
||||
@@ -13,25 +13,17 @@ import Course from './Course';
|
||||
import setupDiscussionSidebar from './test-utils';
|
||||
|
||||
jest.mock('@edx/frontend-platform/analytics');
|
||||
jest.mock('@edx/frontend-lib-special-exams', () => {
|
||||
const actual = jest.requireActual('@edx/frontend-lib-special-exams');
|
||||
return {
|
||||
...actual,
|
||||
__esModule: true,
|
||||
// Mock the default export (SequenceExamWrapper) to just render children
|
||||
// eslint-disable-next-line react/prop-types
|
||||
default: ({ children }) => <div data-testid="sequence-exam-wrapper">{children}</div>,
|
||||
};
|
||||
});
|
||||
const mockLearnerToolsTestId = 'fake-learner-tools';
|
||||
jest.mock('@edx/frontend-lib-special-exams/dist/data/thunks.js', () => ({
|
||||
...jest.requireActual('@edx/frontend-lib-special-exams/dist/data/thunks.js'),
|
||||
checkExamEntry: () => jest.fn(),
|
||||
}));
|
||||
const mockChatTestId = 'fake-chat';
|
||||
jest.mock(
|
||||
'../../plugin-slots/LearnerToolsSlot',
|
||||
() => ({
|
||||
// eslint-disable-next-line react/prop-types
|
||||
LearnerToolsSlot({ courseId }) {
|
||||
return <div className="fake-learner-tools" data-testid={mockLearnerToolsTestId}>LearnerTools contents {courseId} </div>;
|
||||
},
|
||||
}),
|
||||
'./chat/Chat',
|
||||
// eslint-disable-next-line react/prop-types
|
||||
() => function ({ courseId }) {
|
||||
return <div className="fake-chat" data-testid={mockChatTestId}>Chat contents {courseId} </div>;
|
||||
},
|
||||
);
|
||||
|
||||
const recordFirstSectionCelebration = jest.fn();
|
||||
@@ -210,7 +202,7 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('renders course breadcrumbs as expected', async () => {
|
||||
it('doesn\'t renders course breadcrumbs by default', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata');
|
||||
const unitBlocks = Array.from({ length: 3 }).map(() => Factory.build(
|
||||
'block',
|
||||
@@ -218,7 +210,7 @@ describe('Course', () => {
|
||||
{ courseId: courseMetadata.id },
|
||||
));
|
||||
const testStore = await initializeTestStore({
|
||||
courseMetadata, unitBlocks, enableNavigationSidebar: { enable_navigation_sidebar: false },
|
||||
courseMetadata, unitBlocks,
|
||||
}, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
const { courseId, sequenceId } = courseware;
|
||||
@@ -234,10 +226,10 @@ describe('Course', () => {
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText('Loading learning sequence...')).not.toBeInTheDocument();
|
||||
});
|
||||
// expect the section and sequence "titles" to be loaded in as breadcrumb labels.
|
||||
waitFor(() => {
|
||||
expect(screen.findByText(Object.values(models.sections)[0].title)).toBeInTheDocument();
|
||||
expect(screen.findByText(Object.values(models.sequences)[0].title)).toBeInTheDocument();
|
||||
// expect the section and sequence "titles" not to be loaded in as breadcrumb labels.
|
||||
await waitFor(() => {
|
||||
expect(screen.queryByText(Object.values(models.sections)[0].title)).not.toBeInTheDocument();
|
||||
expect(screen.queryByText(Object.values(models.sequences)[0].title)).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -368,27 +360,9 @@ describe('Course', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('displays learner tools when screen is wide enough (browser)', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
enrollment: { mode: 'verified' },
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
const { courseware, models } = testStore.getState();
|
||||
const { courseId, sequenceId } = courseware;
|
||||
const testData = {
|
||||
...mockData,
|
||||
courseId,
|
||||
sequenceId,
|
||||
unitId: Object.values(models.units)[0].id,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||
await waitFor(() => expect(learnerTools).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not display learner tools when screen is too narrow (mobile)', async () => {
|
||||
global.innerWidth = breakpoints.extraSmall.minWidth;
|
||||
it('displays chat when screen is wide enough (browser)', async () => {
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
learning_assistant_enabled: true,
|
||||
enrollment: { mode: 'verified' },
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
@@ -400,7 +374,26 @@ describe('Course', () => {
|
||||
sequenceId,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const learnerTools = screen.queryByTestId(mockLearnerToolsTestId);
|
||||
await expect(learnerTools).not.toBeInTheDocument();
|
||||
const chat = screen.queryByTestId(mockChatTestId);
|
||||
waitFor(() => expect(chat).toBeInTheDocument());
|
||||
});
|
||||
|
||||
it('does not display chat when screen is too narrow (mobile)', async () => {
|
||||
global.innerWidth = breakpoints.extraSmall.minWidth;
|
||||
const courseMetadata = Factory.build('courseMetadata', {
|
||||
learning_assistant_enabled: true,
|
||||
enrollment: { mode: 'verified' },
|
||||
});
|
||||
const testStore = await initializeTestStore({ courseMetadata }, false);
|
||||
const { courseware } = testStore.getState();
|
||||
const { courseId, sequenceId } = courseware;
|
||||
const testData = {
|
||||
...mockData,
|
||||
courseId,
|
||||
sequenceId,
|
||||
};
|
||||
render(<Course {...testData} />, { store: testStore, wrapWithRouter: true });
|
||||
const chat = screen.queryByTestId(mockChatTestId);
|
||||
await expect(chat).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
82
src/courseware/course/chat/Chat.jsx
Normal file
@@ -0,0 +1,82 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import { useSelector } from 'react-redux';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { Xpert } from '@edx/frontend-lib-learning-assistant';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import { ALLOW_UPSELL_MODES, VERIFIED_MODES } from '@src/constants';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
const Chat = ({
|
||||
enabled,
|
||||
enrollmentMode,
|
||||
isStaff,
|
||||
courseId,
|
||||
contentToolsEnabled,
|
||||
unitId,
|
||||
}) => {
|
||||
const {
|
||||
activeAttempt, exam,
|
||||
} = useSelector(state => state.specialExams);
|
||||
const course = useModel('coursewareMeta', courseId);
|
||||
|
||||
// If is disabled or taking an exam, we don't show the chat.
|
||||
if (!enabled || activeAttempt?.attempt_id || exam?.id) { return null; }
|
||||
|
||||
// If is not staff and doesn't have an enrollment, we don't show the chat.
|
||||
if (!isStaff && !enrollmentMode) { return null; }
|
||||
|
||||
const verifiedMode = VERIFIED_MODES.includes(enrollmentMode); // Enrollment verified
|
||||
const auditMode = (
|
||||
!isStaff
|
||||
&& !verifiedMode
|
||||
&& ALLOW_UPSELL_MODES.includes(enrollmentMode) // Can upgrade course
|
||||
&& getConfig().ENABLE_XPERT_AUDIT
|
||||
);
|
||||
// If user has no access, we don't show the chat.
|
||||
if (!isStaff && !(verifiedMode || auditMode)) { return null; }
|
||||
|
||||
// Date validation
|
||||
const {
|
||||
accessExpiration,
|
||||
start,
|
||||
end,
|
||||
} = course;
|
||||
|
||||
const utcDate = (new Date()).toISOString();
|
||||
const expiration = accessExpiration?.expirationDate || utcDate;
|
||||
const validDate = (
|
||||
(start ? start <= utcDate : true)
|
||||
&& (end ? end >= utcDate : true)
|
||||
&& (auditMode ? expiration >= utcDate : true)
|
||||
);
|
||||
// If date is invalid, we don't show the chat.
|
||||
if (!validDate) { return null; }
|
||||
|
||||
// Use a portal to ensure that component overlay does not compete with learning MFE styles.
|
||||
return createPortal(
|
||||
<Xpert
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={contentToolsEnabled}
|
||||
unitId={unitId}
|
||||
isUpgradeEligible={auditMode}
|
||||
/>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
Chat.propTypes = {
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
enabled: PropTypes.bool.isRequired,
|
||||
enrollmentMode: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
contentToolsEnabled: PropTypes.bool.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
Chat.defaultProps = {
|
||||
enrollmentMode: null,
|
||||
};
|
||||
|
||||
export default Chat;
|
||||
286
src/courseware/course/chat/Chat.test.jsx
Normal file
@@ -0,0 +1,286 @@
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import React from 'react';
|
||||
import { Factory } from 'rosie';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
|
||||
import {
|
||||
initializeMockApp,
|
||||
initializeTestStore,
|
||||
render,
|
||||
screen,
|
||||
} from '../../../setupTest';
|
||||
|
||||
import Chat from './Chat';
|
||||
|
||||
// We do a partial mock to avoid mocking out other exported values (e.g. the reducer).
|
||||
// We mock out the Xpert component, because the Xpert component has its own rules for whether it renders
|
||||
// or not, and this includes the results of API calls it makes. We don't want to test those rules here, just
|
||||
// whether the Xpert is rendered by the Chat component in certain conditions. Instead of actually rendering
|
||||
// Xpert, we render and assert on a mocked component.
|
||||
const mockXpertTestId = 'xpert';
|
||||
|
||||
jest.mock('@edx/frontend-lib-learning-assistant', () => {
|
||||
const originalModule = jest.requireActual('@edx/frontend-lib-learning-assistant');
|
||||
|
||||
return {
|
||||
__esModule: true,
|
||||
...originalModule,
|
||||
Xpert: () => (<div data-testid={mockXpertTestId}>mocked Xpert</div>),
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn().mockReturnValue({ ENABLE_XPERT_AUDIT: false }),
|
||||
}));
|
||||
|
||||
initializeMockApp();
|
||||
|
||||
const courseId = 'course-v1:edX+DemoX+Demo_Course';
|
||||
let testCases = [];
|
||||
let enabledTestCases = [];
|
||||
let disabledTestCases = [];
|
||||
const enabledModes = [
|
||||
'professional', 'verified', 'no-id-professional', 'credit', 'masters', 'executive-education',
|
||||
'paid-executive-education', 'paid-bootcamp',
|
||||
];
|
||||
const disabledModes = [null, undefined, 'xyz', 'audit', 'honor', 'unpaid-executive-education', 'unpaid-bootcamp'];
|
||||
|
||||
describe('Chat', () => {
|
||||
let store;
|
||||
|
||||
beforeAll(async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: null,
|
||||
},
|
||||
exam: {
|
||||
id: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Generate test cases.
|
||||
enabledTestCases = enabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
||||
disabledTestCases = disabledModes.map((mode) => ({ enrollmentMode: mode, isVisible: false }));
|
||||
testCases = enabledTestCases.concat(disabledTestCases);
|
||||
|
||||
testCases.forEach(test => {
|
||||
it(
|
||||
`visibility determined by ${test.enrollmentMode} enrollment mode when enabled and not isStaff`,
|
||||
async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
// Generate test cases.
|
||||
testCases = enabledModes.concat(disabledModes).map((mode) => ({ enrollmentMode: mode, isVisible: true }));
|
||||
testCases.forEach(test => {
|
||||
it('visibility determined by isStaff when enabled and any enrollment mode', async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// Generate the map function used for generating test cases by currying the map function.
|
||||
// In this test suite, visibility depends on whether the enrollment mode is a valid or invalid
|
||||
// enrollment mode for enabling the Chat when the user is not a staff member and the Chat is enabled. Instead of
|
||||
// defining two separate map functions that differ in only one case, curry the function.
|
||||
const generateMapFunction = (areEnabledModes) => (
|
||||
(mode) => (
|
||||
[
|
||||
{
|
||||
enrollmentMode: mode, isStaff: true, enabled: true, isVisible: true,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: true, enabled: false, isVisible: false,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: false, enabled: true, isVisible: areEnabledModes,
|
||||
},
|
||||
{
|
||||
enrollmentMode: mode, isStaff: false, enabled: false, isVisible: false,
|
||||
},
|
||||
]
|
||||
)
|
||||
);
|
||||
|
||||
// Generate test cases.
|
||||
enabledTestCases = enabledModes.map(generateMapFunction(true));
|
||||
disabledTestCases = disabledModes.map(generateMapFunction(false));
|
||||
testCases = enabledTestCases.concat(disabledTestCases);
|
||||
testCases = testCases.flat();
|
||||
testCases.forEach(test => {
|
||||
it(
|
||||
`visibility determined by ${test.enabled} enabled when ${test.isStaff} isStaff
|
||||
and ${test.enrollmentMode} enrollment mode`,
|
||||
async () => {
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode={test.enrollmentMode}
|
||||
isStaff={test.isStaff}
|
||||
enabled={test.enabled}
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
if (test.isVisible) {
|
||||
expect(chat).toBeInTheDocument();
|
||||
} else {
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
}
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
it('if course end date has passed, component should not be visible', async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: 1,
|
||||
},
|
||||
},
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
start: '2014-02-03T05:00:00Z',
|
||||
end: '2014-02-05T05:00:00Z',
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="verified"
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('if learner has active exam attempt, component should not be visible', async () => {
|
||||
store = await initializeTestStore({
|
||||
specialExams: {
|
||||
activeAttempt: {
|
||||
attempt_id: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="verified"
|
||||
isStaff
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('displays component for audit learner if explicitly enabled', async () => {
|
||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
||||
|
||||
store = await initializeTestStore({
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
access_expiration: { expiration_date: '' },
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="audit"
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not display component for audit learner if access deadline has passed', async () => {
|
||||
getConfig.mockImplementation(() => ({ ENABLE_XPERT_AUDIT: true }));
|
||||
|
||||
store = await initializeTestStore({
|
||||
courseMetadata: Factory.build('courseMetadata', {
|
||||
access_expiration: { expiration_date: '2014-02-03T05:00:00Z' },
|
||||
}),
|
||||
});
|
||||
|
||||
render(
|
||||
<BrowserRouter>
|
||||
<Chat
|
||||
enrollmentMode="audit"
|
||||
isStaff={false}
|
||||
enabled
|
||||
courseId={courseId}
|
||||
contentToolsEnabled={false}
|
||||
/>
|
||||
</BrowserRouter>,
|
||||
{ store },
|
||||
);
|
||||
|
||||
const chat = screen.queryByTestId(mockXpertTestId);
|
||||
expect(chat).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
1
src/courseware/course/chat/index.js
Normal file
@@ -0,0 +1 @@
|
||||
export { default } from './Chat';
|
||||
@@ -18,8 +18,8 @@ import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
import CelebrationMobile from './assets/celebration_456x328.gif';
|
||||
import CelebrationDesktop from './assets/celebration_750x540.gif';
|
||||
import certificate from '../../../generic/assets/edX_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/edX_locked_certificate.png';
|
||||
import certificate from '../../../generic/assets/openedx_certificate.png';
|
||||
import certificateLocked from '../../../generic/assets/openedx_locked_certificate.png';
|
||||
import { FormattedPricing } from '../../../generic/upgrade-button';
|
||||
import messages from './messages';
|
||||
import { useModel } from '../../../generic/model-store';
|
||||
|
||||
@@ -4,7 +4,7 @@ import PropTypes from 'prop-types';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
|
||||
import { Alert, Button, Hyperlink } from '@openedx/paragon';
|
||||
import certImage from '../../../generic/assets/edX_certificate.png';
|
||||
import certImage from '../../../generic/assets/openedx_certificate.png';
|
||||
import messages from './messages';
|
||||
|
||||
/**
|
||||
|
||||
@@ -17,6 +17,7 @@ const CELEBRATION_STATUSES = [
|
||||
'audit_passing',
|
||||
'downloadable',
|
||||
'earned_but_not_available',
|
||||
'not_earned_but_available_date',
|
||||
'honor_passing',
|
||||
'requesting',
|
||||
'unverified',
|
||||
|
||||
@@ -19,7 +19,6 @@ import { CourseOutlineSidebarTriggerSlot } from '@src/plugin-slots/CourseOutline
|
||||
import { NotificationsDiscussionsSidebarSlot } from '@src/plugin-slots/NotificationsDiscussionsSidebarSlot';
|
||||
import SequenceNavigationSlot from '@src/plugin-slots/SequenceNavigationSlot';
|
||||
|
||||
import { getCoursewareOutlineSidebarSettings } from '../../data/selectors';
|
||||
import CourseLicense from '../course-license';
|
||||
import messages from './messages';
|
||||
import HiddenAfterDue from './hidden-after-due';
|
||||
@@ -48,7 +47,7 @@ const Sequence = ({
|
||||
const unit = useModel('units', unitId);
|
||||
const sequenceStatus = useSelector(state => state.courseware.sequenceStatus);
|
||||
const sequenceMightBeUnit = useSelector(state => state.courseware.sequenceMightBeUnit);
|
||||
const { enableNavigationSidebar: isEnabledOutlineSidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
|
||||
const handleNext = () => {
|
||||
const nextIndex = sequence.unitIds.indexOf(unitId) + 1;
|
||||
const newUnitId = sequence.unitIds[nextIndex];
|
||||
@@ -91,6 +90,30 @@ const Sequence = ({
|
||||
sendTrackingLogEvent(eventName, payload);
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const nextHandler = () => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const previousHandler = () => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
};
|
||||
|
||||
/* istanbul ignore next */
|
||||
const onNavigate = (destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
};
|
||||
|
||||
const sequenceNavProps = {
|
||||
nextHandler,
|
||||
previousHandler,
|
||||
onNavigate,
|
||||
};
|
||||
|
||||
useSequenceBannerTextAlert(sequenceId);
|
||||
useSequenceEntranceExamAlert(courseId, sequenceId, intl);
|
||||
|
||||
@@ -171,30 +194,25 @@ const Sequence = ({
|
||||
/>
|
||||
<CourseOutlineSidebarSlot />
|
||||
<div className="sequence w-100">
|
||||
{!isEnabledOutlineSidebar && (
|
||||
<div className="sequence-navigation-container">
|
||||
<SequenceNavigationSlot
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
nextHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.next_selected', 'top');
|
||||
handleNext();
|
||||
}}
|
||||
onNavigate={(destinationUnitId) => {
|
||||
logEvent('edx.ui.lms.sequence.tab_selected', 'top', destinationUnitId);
|
||||
handleNavigate(destinationUnitId);
|
||||
}}
|
||||
previousHandler={() => {
|
||||
logEvent('edx.ui.lms.sequence.previous_selected', 'top');
|
||||
handlePrevious();
|
||||
}}
|
||||
{...{
|
||||
nextSequenceHandler,
|
||||
handleNavigate,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div className="sequence-navigation-container">
|
||||
{/**
|
||||
SequenceNavigationSlot renders nothing by default.
|
||||
However, we still pass nextHandler, previousHandler, and onNavigate,
|
||||
because, as per the slot's contract, if this slot is replaced
|
||||
with the default SequenceNavigation component, these props are required.
|
||||
These handlers are excluded from test coverage via istanbul ignore,
|
||||
since they are not used unless the slot is overridden.
|
||||
*/}
|
||||
<SequenceNavigationSlot
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
{...{
|
||||
...sequenceNavProps,
|
||||
nextSequenceHandler,
|
||||
handleNavigate,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="unit-container flex-grow-1 pt-4">
|
||||
<SequenceContent
|
||||
@@ -204,7 +222,6 @@ const Sequence = ({
|
||||
unitId={unitId}
|
||||
unitLoadedHandler={handleUnitLoaded}
|
||||
isOriginalUserStaff={originalUserIsStaff}
|
||||
isEnabledOutlineSidebar={isEnabledOutlineSidebar}
|
||||
renderUnitNavigation={renderUnitNavigation}
|
||||
/>
|
||||
{unitHasLoaded && renderUnitNavigation(false)}
|
||||
|
||||
@@ -24,7 +24,6 @@ describe('Sequence', () => {
|
||||
{ type: 'vertical' },
|
||||
{ courseId: courseMetadata.id },
|
||||
));
|
||||
const enableNavigationSidebar = { enable_navigation_sidebar: false };
|
||||
|
||||
beforeAll(async () => {
|
||||
const store = await initializeTestStore({ courseMetadata, unitBlocks });
|
||||
@@ -96,7 +95,6 @@ describe('Sequence', () => {
|
||||
unitBlocks,
|
||||
sequenceBlocks,
|
||||
sequenceMetadata,
|
||||
enableNavigationSidebar: { enable_navigation_sidebar: true },
|
||||
}, false);
|
||||
const { container } = render(
|
||||
<SidebarWrapper overrideData={{ sequenceId: sequenceBlocks[0].id }} />,
|
||||
@@ -131,7 +129,7 @@ describe('Sequence', () => {
|
||||
{ courseId: courseMetadata.id, unitBlocks, sequenceBlock: sequenceBlocks[0] },
|
||||
)];
|
||||
const testStore = await initializeTestStore({
|
||||
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata, enableNavigationSidebar,
|
||||
courseMetadata, unitBlocks, sequenceBlocks, sequenceMetadata,
|
||||
}, false);
|
||||
render(
|
||||
<Sequence {...mockData} {...{ sequenceId: sequenceBlocks[0].id }} />,
|
||||
@@ -190,7 +188,7 @@ describe('Sequence', () => {
|
||||
|
||||
beforeAll(async () => {
|
||||
testStore = await initializeTestStore({
|
||||
courseMetadata, unitBlocks, sequenceBlocks, enableNavigationSidebar,
|
||||
courseMetadata, unitBlocks, sequenceBlocks,
|
||||
}, false);
|
||||
});
|
||||
|
||||
@@ -366,7 +364,6 @@ describe('Sequence', () => {
|
||||
unitBlocks,
|
||||
sequenceBlocks: testSequenceBlocks,
|
||||
sequenceMetadata: testSequenceMetadata,
|
||||
enableNavigationSidebar,
|
||||
}, false);
|
||||
const testData = {
|
||||
...mockData,
|
||||
|
||||
@@ -16,7 +16,6 @@ const SequenceContent = ({
|
||||
unitId,
|
||||
unitLoadedHandler,
|
||||
isOriginalUserStaff,
|
||||
isEnabledOutlineSidebar,
|
||||
renderUnitNavigation,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
@@ -63,7 +62,6 @@ const SequenceContent = ({
|
||||
id={unitId}
|
||||
onLoaded={unitLoadedHandler}
|
||||
isOriginalUserStaff={isOriginalUserStaff}
|
||||
isEnabledOutlineSidebar={isEnabledOutlineSidebar}
|
||||
renderUnitNavigation={renderUnitNavigation}
|
||||
/>
|
||||
);
|
||||
@@ -76,7 +74,6 @@ SequenceContent.propTypes = {
|
||||
unitId: PropTypes.string,
|
||||
unitLoadedHandler: PropTypes.func.isRequired,
|
||||
isOriginalUserStaff: PropTypes.bool.isRequired,
|
||||
isEnabledOutlineSidebar: PropTypes.bool.isRequired,
|
||||
renderUnitNavigation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ describe('Sequence Content', () => {
|
||||
sequenceId: courseware.sequenceId,
|
||||
unitId: models.sequences[courseware.sequenceId].unitIds[0],
|
||||
unitLoadedHandler: () => { },
|
||||
renderUnitNavigation: () => { },
|
||||
};
|
||||
});
|
||||
|
||||
@@ -38,7 +39,7 @@ describe('Sequence Content', () => {
|
||||
});
|
||||
|
||||
it('displays message for no content', () => {
|
||||
render(<SequenceContent {...mockData} unitId={null} />, { wrapWithRouter: true });
|
||||
render(<SequenceContent {...mockData} unitId="" />, { wrapWithRouter: true });
|
||||
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
import { ModalDialog } from '@openedx/paragon';
|
||||
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
|
||||
import { ContentIFrameErrorSlot } from '../../../../plugin-slots/ContentIFrameErrorSlot';
|
||||
|
||||
import * as hooks from './hooks';
|
||||
|
||||
@@ -66,11 +66,7 @@ const ContentIFrame = ({
|
||||
return (
|
||||
<>
|
||||
{(shouldShowContent && !hasLoaded) && (
|
||||
showError ? (
|
||||
<ContentIFrameErrorSlot courseId={courseId} />
|
||||
) : (
|
||||
<ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
|
||||
)
|
||||
showError ? <ErrorPage /> : <ContentIFrameLoaderSlot courseId={courseId} loadingMessage={loadingMessage} />
|
||||
)}
|
||||
{shouldShowContent && (
|
||||
<div className="unit-iframe-wrapper">
|
||||
@@ -80,7 +76,7 @@ const ContentIFrame = ({
|
||||
{modalOptions.isOpen
|
||||
&& (
|
||||
<ModalDialog
|
||||
className="modal-lti"
|
||||
dialogClassName="modal-lti"
|
||||
onClose={handleModalClose}
|
||||
size={modalOptions.isFullscreen ? 'fullscreen' : 'md'}
|
||||
isOpen
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
|
||||
import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
@@ -9,10 +8,13 @@ import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import { fetchCourse } from '@src/courseware/data';
|
||||
import { processEvent } from '@src/course-home/data/thunks';
|
||||
import { useEventListener } from '@src/generic/hooks';
|
||||
import { useSequenceNavigationMetadata } from '@src/courseware/course/sequence/sequence-navigation/hooks';
|
||||
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
import useIFrameBehavior, { stateKeys } from './useIFrameBehavior';
|
||||
import useIFrameBehavior, { iframeBehaviorState } from './useIFrameBehavior';
|
||||
|
||||
const mockNavigate = jest.fn();
|
||||
|
||||
jest.mock('@edx/frontend-platform', () => ({
|
||||
getConfig: jest.fn(),
|
||||
@@ -22,21 +24,14 @@ jest.mock('@edx/frontend-platform/analytics');
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: jest.fn(),
|
||||
useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })),
|
||||
}));
|
||||
|
||||
jest.mock('react-redux', () => ({
|
||||
useDispatch: jest.fn(),
|
||||
useSelector: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock('lodash', () => ({
|
||||
...jest.requireActual('lodash'),
|
||||
throttle: jest.fn((fn) => fn),
|
||||
}));
|
||||
|
||||
jest.mock('./useLoadBearingHook', () => jest.fn());
|
||||
|
||||
jest.mock('@edx/frontend-platform/logging', () => ({
|
||||
logError: jest.fn(),
|
||||
}));
|
||||
@@ -50,8 +45,16 @@ jest.mock('@src/course-home/data/thunks', () => ({
|
||||
jest.mock('@src/generic/hooks', () => ({
|
||||
useEventListener: jest.fn(),
|
||||
}));
|
||||
jest.mock('@src/generic/model-store', () => ({
|
||||
useModel: () => ({ unitIds: ['unit1', 'unit2'], entranceExamData: { entranceExamPassed: null } }),
|
||||
}));
|
||||
jest.mock('react-router-dom', () => ({
|
||||
...jest.requireActual('react-router-dom'),
|
||||
useNavigate: () => mockNavigate,
|
||||
}));
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
jest.mock('@src/courseware/course/sequence/sequence-navigation/hooks');
|
||||
useSequenceNavigationMetadata.mockReturnValue({ isLastUnit: false, nextLink: '/next-unit-link' });
|
||||
|
||||
const props = {
|
||||
elementId: 'test-element-id',
|
||||
@@ -90,148 +93,147 @@ const stateVals = {
|
||||
windowTopOffset: 32,
|
||||
};
|
||||
|
||||
const setIframeHeight = jest.fn();
|
||||
const setHasLoaded = jest.fn();
|
||||
const setShowError = jest.fn();
|
||||
const setWindowTopOffset = jest.fn();
|
||||
|
||||
const mockState = (state) => {
|
||||
const {
|
||||
iframeHeight, hasLoaded, showError, windowTopOffset,
|
||||
} = state;
|
||||
if ('iframeHeight' in state) { jest.spyOn(iframeBehaviorState, 'iframeHeight').mockImplementation(() => [iframeHeight, setIframeHeight]); }
|
||||
if ('hasLoaded' in state) { jest.spyOn(iframeBehaviorState, 'hasLoaded').mockImplementation(() => [hasLoaded, setHasLoaded]); }
|
||||
if ('showError' in state) { jest.spyOn(iframeBehaviorState, 'showError').mockImplementation(() => [showError, setShowError]); }
|
||||
if ('windowTopOffset' in state) { jest.spyOn(iframeBehaviorState, 'windowTopOffset').mockImplementation(() => [windowTopOffset, setWindowTopOffset]); }
|
||||
};
|
||||
|
||||
describe('useIFrameBehavior hook', () => {
|
||||
let hook;
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
global.document.getElementById = mockGetElementById;
|
||||
global.window.addEventListener = jest.fn();
|
||||
global.window.removeEventListener = jest.fn();
|
||||
global.window.innerHeight = 800;
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes iframe height to 0 and error/loaded values to false', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
state.expectInitializedWith(stateKeys.iframeHeight, 0);
|
||||
state.expectInitializedWith(stateKeys.hasLoaded, false);
|
||||
state.expectInitializedWith(stateKeys.showError, false);
|
||||
state.expectInitializedWith(stateKeys.windowTopOffset, null);
|
||||
mockState(defaultStateVals);
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
|
||||
expect(result.current.iframeHeight).toBe(0);
|
||||
expect(result.current.showError).toBe(false);
|
||||
expect(result.current.hasLoaded).toBe(false);
|
||||
});
|
||||
describe('effects - on frame change', () => {
|
||||
let oldGetElement;
|
||||
beforeEach(() => {
|
||||
global.window ??= Object.create(window);
|
||||
Object.defineProperty(window, 'location', { value: {}, writable: true });
|
||||
state.mockVals(stateVals);
|
||||
oldGetElement = document.getElementById;
|
||||
document.getElementById = mockGetElementById;
|
||||
mockState(defaultStateVals);
|
||||
});
|
||||
afterEach(() => {
|
||||
state.resetVals();
|
||||
jest.clearAllMocks();
|
||||
document.getElementById = oldGetElement;
|
||||
});
|
||||
it('does not post url hash if the window does not have one', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
const cb = getEffects([
|
||||
props.id,
|
||||
props.onLoaded,
|
||||
testIFrameHeight,
|
||||
true,
|
||||
], React)[0];
|
||||
cb();
|
||||
window.location.hash = '';
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
expect(postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
it('posts url hash if the window has one', () => {
|
||||
window.location.hash = testHash;
|
||||
hook = useIFrameBehavior(props);
|
||||
const cb = getEffects([
|
||||
props.id,
|
||||
props.onLoaded,
|
||||
testIFrameHeight,
|
||||
true,
|
||||
], React)[0];
|
||||
cb();
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
expect(postMessage).toHaveBeenCalledWith({ hashName: testHash }, config.LMS_BASE_URL);
|
||||
});
|
||||
});
|
||||
describe('event listener', () => {
|
||||
it('calls eventListener with prepared callback', () => {
|
||||
state.mockVals(stateVals);
|
||||
hook = useIFrameBehavior(props);
|
||||
mockState(stateVals);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const [call] = useEventListener.mock.calls;
|
||||
expect(call[0]).toEqual('message');
|
||||
expect(call[1].prereqs).toEqual([
|
||||
props.id,
|
||||
props.onLoaded,
|
||||
state.values.hasLoaded,
|
||||
state.setState.hasLoaded,
|
||||
state.values.iframeHeight,
|
||||
state.setState.iframeHeight,
|
||||
state.values.windowTopOffset,
|
||||
state.setState.windowTopOffset,
|
||||
stateVals.hasLoaded,
|
||||
setHasLoaded,
|
||||
stateVals.iframeHeight,
|
||||
setIframeHeight,
|
||||
stateVals.windowTopOffset,
|
||||
setWindowTopOffset,
|
||||
]);
|
||||
});
|
||||
describe('resize message', () => {
|
||||
const resizeMessage = (height = 23) => ({
|
||||
const customHeight = 25;
|
||||
const defaultHeight = 23;
|
||||
const resizeMessage = (height = defaultHeight) => ({
|
||||
data: { type: messageTypes.resize, payload: { height } },
|
||||
});
|
||||
const videoFullScreenMessage = (open = false) => ({
|
||||
data: { type: messageTypes.videoFullScreen, payload: { open } },
|
||||
});
|
||||
const testSetIFrameHeight = (height = 23) => {
|
||||
const testSetIFrameHeight = (height = defaultHeight) => {
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage(height));
|
||||
expect(state.setState.iframeHeight).toHaveBeenCalledWith(height);
|
||||
};
|
||||
const testOnlySetsHeight = () => {
|
||||
it('sets iframe height with payload height', () => {
|
||||
testSetIFrameHeight();
|
||||
});
|
||||
it('does not set hasLoaded', () => {
|
||||
expect(state.setState.hasLoaded).not.toHaveBeenCalled();
|
||||
});
|
||||
expect(setIframeHeight).toHaveBeenCalledWith(height);
|
||||
};
|
||||
describe('hasLoaded', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
hook = useIFrameBehavior(props);
|
||||
it('sets iframe height with payload height', () => {
|
||||
mockState({ ...defaultStateVals, hasLoaded: true });
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage(customHeight));
|
||||
expect(setIframeHeight).toHaveBeenCalledWith(0);
|
||||
expect(setIframeHeight).toHaveBeenCalledWith(customHeight);
|
||||
expect(setIframeHeight).not.toHaveBeenCalledWith(defaultHeight);
|
||||
});
|
||||
testOnlySetsHeight();
|
||||
});
|
||||
describe('iframeHeight is not 0', () => {
|
||||
beforeEach(() => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
hook = useIFrameBehavior(props);
|
||||
});
|
||||
testOnlySetsHeight();
|
||||
});
|
||||
describe('payload height is 0', () => {
|
||||
beforeEach(() => { hook = useIFrameBehavior(props); });
|
||||
testOnlySetsHeight(0);
|
||||
it('sets iframe height with payload height', () => {
|
||||
mockState(defaultStateVals);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage(0));
|
||||
expect(setIframeHeight).toHaveBeenCalledWith(0);
|
||||
expect(setIframeHeight).not.toHaveBeenCalledWith(customHeight);
|
||||
expect(setIframeHeight).not.toHaveBeenCalledWith(defaultHeight);
|
||||
});
|
||||
});
|
||||
describe('payload is present but uninitialized', () => {
|
||||
beforeEach(() => {
|
||||
mockState(defaultStateVals);
|
||||
});
|
||||
it('sets iframe height with payload height', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
testSetIFrameHeight();
|
||||
});
|
||||
it('sets hasLoaded and calls onLoaded', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage());
|
||||
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
|
||||
expect(setHasLoaded).toHaveBeenCalledWith(true);
|
||||
expect(props.onLoaded).toHaveBeenCalled();
|
||||
});
|
||||
test('onLoaded is optional', () => {
|
||||
hook = useIFrameBehavior({ ...props, onLoaded: undefined });
|
||||
renderHook(() => useIFrameBehavior({ ...props, onLoaded: undefined }));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage());
|
||||
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
|
||||
expect(setHasLoaded).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
it('scrolls to current window vertical offset if one is set', () => {
|
||||
const windowTopOffset = 32;
|
||||
state.mockVals({ ...defaultStateVals, windowTopOffset });
|
||||
hook = useIFrameBehavior(props);
|
||||
mockState({ ...defaultStateVals, windowTopOffset });
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(videoFullScreenMessage());
|
||||
expect(window.scrollTo).toHaveBeenCalledWith(0, windowTopOffset);
|
||||
});
|
||||
it('does not scroll if towverticalp offset is not set', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
cb(resizeMessage());
|
||||
expect(window.scrollTo).not.toHaveBeenCalled();
|
||||
@@ -245,16 +247,16 @@ describe('useIFrameBehavior hook', () => {
|
||||
});
|
||||
beforeEach(() => {
|
||||
window.scrollY = scrollY;
|
||||
hook = useIFrameBehavior(props);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
[[, { cb }]] = useEventListener.mock.calls;
|
||||
});
|
||||
it('sets window top offset based on window.scrollY if opening the video', () => {
|
||||
cb(fullScreenMessage(true));
|
||||
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(scrollY);
|
||||
expect(setWindowTopOffset).toHaveBeenCalledWith(scrollY);
|
||||
});
|
||||
it('sets window top offset to null if closing the video', () => {
|
||||
cb(fullScreenMessage(false));
|
||||
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(null);
|
||||
expect(setWindowTopOffset).toHaveBeenCalledWith(null);
|
||||
});
|
||||
});
|
||||
describe('offset message', () => {
|
||||
@@ -266,7 +268,7 @@ describe('useIFrameBehavior hook', () => {
|
||||
document.getElementById = mockGetEl;
|
||||
const oldScrollTo = window.scrollTo;
|
||||
window.scrollTo = jest.fn();
|
||||
hook = useIFrameBehavior(props);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
const offset = 99;
|
||||
cb({ data: { offset } });
|
||||
@@ -278,18 +280,85 @@ describe('useIFrameBehavior hook', () => {
|
||||
});
|
||||
});
|
||||
describe('visibility tracking', () => {
|
||||
it('sets up visibility tracking after iframe has loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
useIFrameBehavior(props);
|
||||
it('sets up visibility tracking after iframe loads', () => {
|
||||
mockState({ ...defaultStateVals, hasLoaded: true });
|
||||
|
||||
const effects = getEffects([true, props.elementId], React);
|
||||
expect(effects.length).toEqual(2);
|
||||
effects[0](); // Execute the visibility tracking effect.
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
|
||||
expect(global.window.addEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(global.window.addEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
|
||||
expect(global.window.addEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
|
||||
// Initial visibility update.
|
||||
// Initial visibility update is handled by the `handleIFrameLoad` method.
|
||||
expect(postMessage).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({ type: 'unit.visibilityStatus' }),
|
||||
config.LMS_BASE_URL,
|
||||
);
|
||||
});
|
||||
it('does not set up visibility tracking before iframe has loaded', () => {
|
||||
window.location.hash = ''; // Avoid posting hash message.
|
||||
mockState({ ...defaultStateVals, hasLoaded: false });
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
|
||||
expect(global.window.addEventListener).not.toHaveBeenCalled();
|
||||
expect(postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
it('cleans up event listeners on unmount', () => {
|
||||
mockState({ ...defaultStateVals, hasLoaded: true });
|
||||
const { unmount } = renderHook(() => useIFrameBehavior(props));
|
||||
|
||||
unmount(); // Call the cleanup function.
|
||||
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleIFrameLoad', () => {
|
||||
it('sets and logs error if has not loaded', () => {
|
||||
mockState(defaultStateVals);
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
expect(setShowError).toHaveBeenCalledWith(true);
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
it('sends track event if has not loaded', () => {
|
||||
mockState(defaultStateVals);
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
const eventName = 'edx.bi.error.learning.iframe_load_failed';
|
||||
const eventProperties = {
|
||||
unitId: props.id,
|
||||
iframeUrl: props.iframeUrl,
|
||||
};
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(eventName, eventProperties);
|
||||
});
|
||||
it('does not set/log errors if loaded', () => {
|
||||
mockState({ ...defaultStateVals, hasLoaded: true });
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
expect(setShowError).not.toHaveBeenCalled();
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not send track event if loaded', () => {
|
||||
mockState({ ...defaultStateVals, hasLoaded: true });
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
expect(sendTrackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('registers an event handler to process fetchCourse events.', () => {
|
||||
mockState(defaultStateVals);
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
const eventName = 'test-event-name';
|
||||
const event = { data: { event_name: eventName } };
|
||||
window.onmessage(event);
|
||||
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
|
||||
});
|
||||
it('updates initial iframe visibility on load', () => {
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
result.current.handleIFrameLoad();
|
||||
expect(postMessage).toHaveBeenCalledWith(
|
||||
{
|
||||
type: 'unit.visibilityStatus',
|
||||
@@ -301,76 +370,37 @@ describe('useIFrameBehavior hook', () => {
|
||||
config.LMS_BASE_URL,
|
||||
);
|
||||
});
|
||||
it('does not set up visibility tracking before iframe has loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: false });
|
||||
useIFrameBehavior(props);
|
||||
|
||||
const effects = getEffects([false, props.elementId], React);
|
||||
expect(effects).toBeNull();
|
||||
|
||||
expect(global.window.addEventListener).not.toHaveBeenCalled();
|
||||
expect(postMessage).not.toHaveBeenCalled();
|
||||
});
|
||||
it('cleans up event listeners on unmount', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
useIFrameBehavior(props);
|
||||
|
||||
const effects = getEffects([true, props.elementId], React);
|
||||
const cleanup = effects[0](); // Execute the effect and get the cleanup function.
|
||||
cleanup(); // Call the cleanup function.
|
||||
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
|
||||
expect(global.window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('output', () => {
|
||||
describe('handleIFrameLoad', () => {
|
||||
it('sets and logs error if has not loaded', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
hook.handleIFrameLoad();
|
||||
expect(state.setState.showError).toHaveBeenCalledWith(true);
|
||||
expect(logError).toHaveBeenCalled();
|
||||
});
|
||||
it('sends track event if has not loaded', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
hook.handleIFrameLoad();
|
||||
const eventName = 'edx.bi.error.learning.iframe_load_failed';
|
||||
const eventProperties = {
|
||||
unitId: props.id,
|
||||
iframeUrl: props.iframeUrl,
|
||||
};
|
||||
expect(sendTrackEvent).toHaveBeenCalledWith(eventName, eventProperties);
|
||||
});
|
||||
it('does not set/log errors if loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
hook = useIFrameBehavior(props);
|
||||
hook.handleIFrameLoad();
|
||||
expect(state.setState.showError).not.toHaveBeenCalled();
|
||||
expect(logError).not.toHaveBeenCalled();
|
||||
});
|
||||
it('does not send track event if loaded', () => {
|
||||
state.mockVals({ ...defaultStateVals, hasLoaded: true });
|
||||
hook = useIFrameBehavior(props);
|
||||
hook.handleIFrameLoad();
|
||||
expect(sendTrackEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
it('registers an event handler to process fetchCourse events.', () => {
|
||||
hook = useIFrameBehavior(props);
|
||||
hook.handleIFrameLoad();
|
||||
const eventName = 'test-event-name';
|
||||
const event = { data: { event_name: eventName } };
|
||||
window.onmessage(event);
|
||||
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
|
||||
});
|
||||
});
|
||||
it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => {
|
||||
state.mockVals(stateVals);
|
||||
hook = useIFrameBehavior(props);
|
||||
expect(hook.iframeHeight).toEqual(stateVals.iframeHeight);
|
||||
expect(hook.showError).toEqual(stateVals.showError);
|
||||
expect(hook.hasLoaded).toEqual(stateVals.hasLoaded);
|
||||
mockState(stateVals);
|
||||
const { result } = renderHook(() => useIFrameBehavior(props));
|
||||
expect(result.current.iframeHeight).toBe(stateVals.iframeHeight);
|
||||
expect(result.current.showError).toBe(stateVals.showError);
|
||||
expect(result.current.hasLoaded).toBe(stateVals.hasLoaded);
|
||||
});
|
||||
});
|
||||
describe('navigate link for the next unit on auto advance', () => {
|
||||
it('test for link when it is not last unit', () => {
|
||||
mockState(defaultStateVals);
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
const autoAdvanceMessage = () => ({
|
||||
data: { type: messageTypes.autoAdvance },
|
||||
});
|
||||
cb(autoAdvanceMessage());
|
||||
expect(mockNavigate).toHaveBeenCalledWith('/next-unit-link');
|
||||
});
|
||||
it('test for link when it is last unit', () => {
|
||||
mockState(defaultStateVals);
|
||||
useSequenceNavigationMetadata.mockReset();
|
||||
useSequenceNavigationMetadata.mockReturnValue({ isLastUnit: true, nextLink: '/next-unit-link' });
|
||||
renderHook(() => useIFrameBehavior(props));
|
||||
const { cb } = useEventListener.mock.calls[0][1];
|
||||
const autoAdvanceMessage = () => ({
|
||||
data: { type: messageTypes.autoAdvance },
|
||||
});
|
||||
cb(autoAdvanceMessage());
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import React, { useState } from 'react';
|
||||
import { getConfig } from '@edx/frontend-platform';
|
||||
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
|
||||
import React from 'react';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useDispatch, useSelector } from 'react-redux';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { throttle } from 'lodash';
|
||||
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { logError } from '@edx/frontend-platform/logging';
|
||||
|
||||
import { fetchCourse } from '@src/courseware/data';
|
||||
import { processEvent } from '@src/course-home/data/thunks';
|
||||
import { useEventListener } from '@src/generic/hooks';
|
||||
import { getSequenceId } from '@src/courseware/data/selectors';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { useSequenceNavigationMetadata } from '@src/courseware/course/sequence/sequence-navigation/hooks';
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
import useLoadBearingHook from './useLoadBearingHook';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
iframeHeight: 'iframeHeight',
|
||||
hasLoaded: 'hasLoaded',
|
||||
showError: 'showError',
|
||||
windowTopOffset: 'windowTopOffset',
|
||||
});
|
||||
export const iframeBehaviorState = {
|
||||
iframeHeight: (val) => useState<number>(val), // eslint-disable-line
|
||||
hasLoaded: (val) => useState<boolean>(val), // eslint-disable-line
|
||||
showError: (val) => useState<boolean>(val), // eslint-disable-line
|
||||
windowTopOffset: (val) => useState<number | null>(val), // eslint-disable-line
|
||||
} as const;
|
||||
|
||||
const useIFrameBehavior = ({
|
||||
elementId,
|
||||
@@ -31,23 +34,29 @@ const useIFrameBehavior = ({
|
||||
useLoadBearingHook(id);
|
||||
|
||||
const dispatch = useDispatch();
|
||||
const activeSequenceId = useSelector(getSequenceId);
|
||||
const navigate = useNavigate();
|
||||
const activeSequence = useModel('sequences', activeSequenceId);
|
||||
const activeUnitId = activeSequence.unitIds.length > 0
|
||||
? activeSequence.unitIds[activeSequence.activeUnitIndex] : null;
|
||||
const { isLastUnit, nextLink } = useSequenceNavigationMetadata(activeSequenceId, activeUnitId);
|
||||
|
||||
const [iframeHeight, setIframeHeight] = useKeyedState(stateKeys.iframeHeight, 0);
|
||||
const [hasLoaded, setHasLoaded] = useKeyedState(stateKeys.hasLoaded, false);
|
||||
const [showError, setShowError] = useKeyedState(stateKeys.showError, false);
|
||||
const [windowTopOffset, setWindowTopOffset] = useKeyedState(stateKeys.windowTopOffset, null);
|
||||
const [iframeHeight, setIframeHeight] = iframeBehaviorState.iframeHeight(0);
|
||||
const [hasLoaded, setHasLoaded] = iframeBehaviorState.hasLoaded(false);
|
||||
const [showError, setShowError] = iframeBehaviorState.showError(false);
|
||||
const [windowTopOffset, setWindowTopOffset] = iframeBehaviorState.windowTopOffset(null);
|
||||
|
||||
React.useEffect(() => {
|
||||
const frame = document.getElementById(elementId);
|
||||
const frame = document.getElementById(elementId) as HTMLIFrameElement | null;
|
||||
const { hash } = window.location;
|
||||
if (hash) {
|
||||
// The url hash will be sent to LMS-served iframe in order to find the location of the
|
||||
// hash within the iframe.
|
||||
frame.contentWindow.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`);
|
||||
frame?.contentWindow?.postMessage({ hashName: hash }, `${getConfig().LMS_BASE_URL}`);
|
||||
}
|
||||
}, [id, onLoaded, iframeHeight, hasLoaded]);
|
||||
|
||||
const receiveMessage = React.useCallback(({ data }) => {
|
||||
const receiveMessage = React.useCallback(({ data }: MessageEvent) => {
|
||||
const { type, payload } = data;
|
||||
if (type === messageTypes.resize) {
|
||||
setIframeHeight(payload.height);
|
||||
@@ -71,7 +80,13 @@ const useIFrameBehavior = ({
|
||||
} else if (data.offset) {
|
||||
// We listen for this message from LMS to know when the page needs to
|
||||
// be scrolled to another location on the page.
|
||||
window.scrollTo(0, data.offset + document.getElementById('unit-iframe').offsetTop);
|
||||
window.scrollTo(0, data.offset + document.getElementById('unit-iframe')!.offsetTop);
|
||||
} else if (type === messageTypes.autoAdvance) {
|
||||
// We are listening to autoAdvance message to move to next sequence automatically.
|
||||
// In case it is the last unit we need not do anything.
|
||||
if (!isLastUnit && nextLink) {
|
||||
navigate(nextLink);
|
||||
}
|
||||
}
|
||||
}, [
|
||||
id,
|
||||
@@ -87,37 +102,36 @@ const useIFrameBehavior = ({
|
||||
useEventListener('message', receiveMessage);
|
||||
|
||||
// Send visibility status to the iframe. It's used to mark XBlocks as viewed.
|
||||
const updateIframeVisibility = () => {
|
||||
const iframeElement = document.getElementById(elementId) as HTMLIFrameElement | null;
|
||||
const rect = iframeElement?.getBoundingClientRect();
|
||||
const visibleInfo = {
|
||||
type: 'unit.visibilityStatus',
|
||||
data: {
|
||||
topPosition: rect?.top,
|
||||
viewportHeight: window.innerHeight,
|
||||
},
|
||||
};
|
||||
iframeElement?.contentWindow?.postMessage(
|
||||
visibleInfo,
|
||||
`${getConfig().LMS_BASE_URL}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Set up visibility tracking event listeners.
|
||||
React.useEffect(() => {
|
||||
if (!hasLoaded) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const iframeElement = document.getElementById(elementId);
|
||||
const iframeElement = document.getElementById(elementId) as HTMLIFrameElement | null;
|
||||
if (!iframeElement || !iframeElement.contentWindow) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updateIframeVisibility = () => {
|
||||
const rect = iframeElement.getBoundingClientRect();
|
||||
const visibleInfo = {
|
||||
type: 'unit.visibilityStatus',
|
||||
data: {
|
||||
topPosition: rect.top,
|
||||
viewportHeight: window.innerHeight,
|
||||
},
|
||||
};
|
||||
iframeElement.contentWindow.postMessage(
|
||||
visibleInfo,
|
||||
`${getConfig().LMS_BASE_URL}`,
|
||||
);
|
||||
};
|
||||
|
||||
// Throttle the update function to prevent it from sending too many messages to the iframe.
|
||||
const throttledUpdateVisibility = throttle(updateIframeVisibility, 100);
|
||||
|
||||
// Update the visibility of the iframe in case the element is already visible.
|
||||
updateIframeVisibility();
|
||||
|
||||
// Add event listeners to update the visibility of the iframe when the window is scrolled or resized.
|
||||
window.addEventListener('scroll', throttledUpdateVisibility);
|
||||
window.addEventListener('resize', throttledUpdateVisibility);
|
||||
@@ -152,6 +166,9 @@ const useIFrameBehavior = ({
|
||||
dispatch(processEvent(e.data, fetchCourse));
|
||||
}
|
||||
};
|
||||
|
||||
// Update the visibility of the iframe in case the element is already visible.
|
||||
updateIframeVisibility();
|
||||
};
|
||||
|
||||
React.useEffect(() => {
|
||||
@@ -1,19 +1,11 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist';
|
||||
|
||||
import { useEventListener } from '@src/generic/hooks';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
isOpen: 'isOpen',
|
||||
options: 'options',
|
||||
});
|
||||
|
||||
export const DEFAULT_HEIGHT = '100%';
|
||||
|
||||
const useModalIFrameData = () => {
|
||||
const [isOpen, setIsOpen] = useKeyedState(stateKeys.isOpen, false);
|
||||
const [options, setOptions] = useKeyedState(stateKeys.options, { height: DEFAULT_HEIGHT });
|
||||
const [isOpen, setIsOpen] = React.useState(false);
|
||||
const [options, setOptions] = React.useState({ height: DEFAULT_HEIGHT });
|
||||
|
||||
const handleModalClose = () => {
|
||||
const rootFrame = document.querySelector('iframe');
|
||||
|
||||
@@ -1,74 +1,85 @@
|
||||
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
import React from 'react';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useEventListener } from '@src/generic/hooks';
|
||||
import { messageTypes } from '../constants';
|
||||
|
||||
import useModalIFrameData, { stateKeys, DEFAULT_HEIGHT } from './useModalIFrameData';
|
||||
import useModalIFrameData, { DEFAULT_HEIGHT } from './useModalIFrameData';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })),
|
||||
useState: jest.fn((initialValue) => [initialValue, jest.fn()]),
|
||||
}));
|
||||
jest.mock('@src/generic/hooks', () => ({
|
||||
useEventListener: jest.fn(),
|
||||
}));
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
const setIsOpen = jest.fn();
|
||||
const setOptions = jest.fn();
|
||||
|
||||
const defaultState = {
|
||||
isOpen: false,
|
||||
options: { height: DEFAULT_HEIGHT },
|
||||
};
|
||||
|
||||
const mockUseStateWithValues = (values) => {
|
||||
jest.spyOn(React, 'useState')
|
||||
.mockReturnValueOnce([values.isOpen, setIsOpen])
|
||||
.mockReturnValueOnce([values.options, setOptions]);
|
||||
};
|
||||
|
||||
describe('useModalIFrameData', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
state.mock();
|
||||
});
|
||||
const testHandleModalClose = ({ trigger }) => {
|
||||
const postMessage = jest.fn();
|
||||
document.querySelector = jest.fn().mockReturnValue({ contentWindow: { postMessage } });
|
||||
trigger();
|
||||
state.expectSetStateCalledWith(stateKeys.isOpen, false);
|
||||
expect(React.useState).toHaveBeenNthCalledWith(1, false);
|
||||
expect(postMessage).toHaveBeenCalledWith({ type: 'plugin.modal-close' }, '*');
|
||||
};
|
||||
describe('behavior', () => {
|
||||
it('initializes isOpen to false', () => {
|
||||
useModalIFrameData();
|
||||
state.expectInitializedWith(stateKeys.isOpen, false);
|
||||
});
|
||||
it('initializes options with default height', () => {
|
||||
useModalIFrameData();
|
||||
state.expectInitializedWith(stateKeys.options, { height: DEFAULT_HEIGHT });
|
||||
it('should initialize with modal closed and default height', () => {
|
||||
const { result } = renderHook(() => useModalIFrameData());
|
||||
|
||||
expect(result.current.modalOptions).toEqual({
|
||||
isOpen: false,
|
||||
height: DEFAULT_HEIGHT,
|
||||
});
|
||||
});
|
||||
describe('eventListener', () => {
|
||||
const oldOptions = { some: 'old', options: 'yeah' };
|
||||
const prepareListener = () => {
|
||||
useModalIFrameData();
|
||||
expect(useEventListener).toHaveBeenCalled();
|
||||
const call = useEventListener.mock.calls[0][1];
|
||||
expect(call.prereqs).toEqual([]);
|
||||
return call.cb;
|
||||
};
|
||||
it('consumes modal events and opens sets modal options with open: true', () => {
|
||||
state.mockVals({
|
||||
[stateKeys.isOpen]: false,
|
||||
[stateKeys.options]: oldOptions,
|
||||
mockUseStateWithValues({
|
||||
isOpen: false,
|
||||
options: oldOptions,
|
||||
});
|
||||
renderHook(() => useModalIFrameData());
|
||||
const receiveMessage = prepareListener();
|
||||
const payload = { test: 'values' };
|
||||
receiveMessage({ data: { type: messageTypes.modal, payload } });
|
||||
expect(state.setState.isOpen).toHaveBeenCalledWith(true);
|
||||
expect(state.setState.options).toHaveBeenCalled();
|
||||
const [[setOptionsCb]] = state.setState.options.mock.calls;
|
||||
expect(setIsOpen).toHaveBeenCalledWith(true);
|
||||
expect(setOptions).toHaveBeenCalled();
|
||||
const [[setOptionsCb]] = setOptions.mock.calls;
|
||||
expect(setOptionsCb(oldOptions)).toEqual({ ...oldOptions, ...payload });
|
||||
});
|
||||
it('ignores events with no type', () => {
|
||||
state.mockVals({
|
||||
[stateKeys.isOpen]: false,
|
||||
[stateKeys.options]: oldOptions,
|
||||
});
|
||||
const { result } = renderHook(() => useModalIFrameData());
|
||||
const initialState = result.current.modalOptions;
|
||||
const receiveMessage = prepareListener();
|
||||
const payload = { test: 'values' };
|
||||
receiveMessage({ data: { payload } });
|
||||
expect(state.setState.isOpen).not.toHaveBeenCalled();
|
||||
expect(state.setState.options).not.toHaveBeenCalled();
|
||||
expect(result.current.modalOptions).toEqual(initialState);
|
||||
});
|
||||
it('calls handleModalClose behavior when receiving a "plugin.modal-close" event', () => {
|
||||
renderHook(() => useModalIFrameData());
|
||||
const receiveMessage = prepareListener();
|
||||
testHandleModalClose({
|
||||
trigger: () => {
|
||||
@@ -80,13 +91,14 @@ describe('useModalIFrameData', () => {
|
||||
});
|
||||
describe('output', () => {
|
||||
test('returns handleModalClose callback', () => {
|
||||
mockUseStateWithValues(defaultState);
|
||||
testHandleModalClose({ trigger: useModalIFrameData().handleModalClose });
|
||||
});
|
||||
it('forwards modalOptions from state values', () => {
|
||||
const modalOptions = { test: 'options' };
|
||||
state.mockVals({
|
||||
[stateKeys.options]: modalOptions,
|
||||
[stateKeys.isOpen]: true,
|
||||
mockUseStateWithValues({
|
||||
isOpen: true,
|
||||
options: modalOptions,
|
||||
});
|
||||
expect(useModalIFrameData().modalOptions).toEqual({
|
||||
...modalOptions,
|
||||
|
||||
@@ -1,19 +1,13 @@
|
||||
import React from 'react';
|
||||
|
||||
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils/dist';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
|
||||
import { modelKeys } from '../constants';
|
||||
|
||||
export const stateKeys = StrictDict({
|
||||
shouldDisplay: 'shouldDisplay',
|
||||
});
|
||||
|
||||
/**
|
||||
* @return {bool} should the honor code be displayed?
|
||||
*/
|
||||
const useShouldDisplayHonorCode = ({ id, courseId }) => {
|
||||
const [shouldDisplay, setShouldDisplay] = useKeyedState(stateKeys.shouldDisplay, false);
|
||||
const [shouldDisplay, setShouldDisplay] = React.useState(false);
|
||||
|
||||
const { graded } = useModel(modelKeys.units, id);
|
||||
const { userNeedsIntegritySignature } = useModel(modelKeys.coursewareMeta, courseId);
|
||||
|
||||
@@ -1,22 +1,12 @@
|
||||
import React from 'react';
|
||||
|
||||
import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
|
||||
import useShouldDisplayHonorCode from './useShouldDisplayHonorCode';
|
||||
import { modelKeys } from '../constants';
|
||||
|
||||
import useShouldDisplayHonorCode, { stateKeys } from './useShouldDisplayHonorCode';
|
||||
|
||||
jest.mock('react', () => ({
|
||||
...jest.requireActual('react'),
|
||||
useEffect: jest.fn(),
|
||||
}));
|
||||
jest.mock('@src/generic/model-store', () => ({
|
||||
useModel: jest.fn(),
|
||||
}));
|
||||
|
||||
const state = mockUseKeyedState(stateKeys);
|
||||
|
||||
const props = {
|
||||
id: 'test-id',
|
||||
courseId: 'test-course-id',
|
||||
@@ -28,52 +18,29 @@ const mockModels = (graded, userNeedsIntegritySignature) => {
|
||||
));
|
||||
};
|
||||
|
||||
describe('useShouldDisplayHonorCode hook', () => {
|
||||
describe('useShouldDisplayHonorCode', () => {
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
mockModels(false, false);
|
||||
state.mock();
|
||||
});
|
||||
describe('behavior', () => {
|
||||
it('initializes shouldDisplay to false', () => {
|
||||
useShouldDisplayHonorCode(props);
|
||||
state.expectInitializedWith(stateKeys.shouldDisplay, false);
|
||||
});
|
||||
describe('effect - on userNeedsIntegritySignature', () => {
|
||||
describe('graded and needs integrity signature', () => {
|
||||
it('sets shouldDisplay(true)', () => {
|
||||
mockModels(true, true);
|
||||
useShouldDisplayHonorCode(props);
|
||||
const cb = getEffects([state.setState.shouldDisplay, true], React)[0];
|
||||
cb();
|
||||
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(true);
|
||||
});
|
||||
});
|
||||
describe('not graded', () => {
|
||||
it('sets should not display', () => {
|
||||
mockModels(true, false);
|
||||
useShouldDisplayHonorCode(props);
|
||||
const cb = getEffects([state.setState.shouldDisplay, false], React)[0];
|
||||
cb();
|
||||
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
describe('does not need integrity signature', () => {
|
||||
it('sets should not display', () => {
|
||||
mockModels(false, true);
|
||||
useShouldDisplayHonorCode(props);
|
||||
const cb = getEffects([state.setState.shouldDisplay, true], React)[0];
|
||||
cb();
|
||||
expect(state.setState.shouldDisplay).toHaveBeenCalledWith(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should return false when userNeedsIntegritySignature is false', () => {
|
||||
mockModels(true, false);
|
||||
|
||||
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
describe('output', () => {
|
||||
it('returns shouldDisplay value from state', () => {
|
||||
const testValue = 'test-value';
|
||||
state.mockVal(stateKeys.shouldDisplay, testValue);
|
||||
expect(useShouldDisplayHonorCode(props)).toEqual(testValue);
|
||||
});
|
||||
|
||||
it('should return false when graded is false', () => {
|
||||
mockModels(false, true);
|
||||
|
||||
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
|
||||
expect(result.current).toBe(false);
|
||||
});
|
||||
|
||||
it('should return true when both userNeedsIntegritySignature and graded are true', () => {
|
||||
mockModels(true, true);
|
||||
|
||||
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
|
||||
expect(result.current).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -22,7 +22,6 @@ const Unit = ({
|
||||
onLoaded,
|
||||
id,
|
||||
isOriginalUserStaff,
|
||||
isEnabledOutlineSidebar,
|
||||
renderUnitNavigation,
|
||||
}) => {
|
||||
const { formatMessage } = useIntl();
|
||||
@@ -48,7 +47,7 @@ const Unit = ({
|
||||
|
||||
return (
|
||||
<div className="unit">
|
||||
<UnitTitleSlot unitId={id} {...{ unit, isEnabledOutlineSidebar, renderUnitNavigation }} />
|
||||
<UnitTitleSlot unitId={id} {...{ unit, renderUnitNavigation }} />
|
||||
<UnitSuspense {...{ courseId, id }} />
|
||||
<ContentIFrame
|
||||
elementId="unit-iframe"
|
||||
@@ -70,7 +69,6 @@ Unit.propTypes = {
|
||||
id: PropTypes.string.isRequired,
|
||||
onLoaded: PropTypes.func,
|
||||
isOriginalUserStaff: PropTypes.bool.isRequired,
|
||||
isEnabledOutlineSidebar: PropTypes.bool.isRequired,
|
||||
renderUnitNavigation: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ const defaultProps = {
|
||||
onLoaded: jest.fn().mockName('props.onLoaded'),
|
||||
id: 'unit-id',
|
||||
isOriginalUserStaff: false,
|
||||
isEnabledOutlineSidebar: false,
|
||||
renderUnitNavigation: jest.fn(enabled => enabled && 'UnitNaviagtion'),
|
||||
};
|
||||
|
||||
@@ -68,35 +67,14 @@ describe('<Unit />', () => {
|
||||
expect(screen.getByText('Bookmark this page')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('does not render unit navigation buttons', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const nextButton = screen.queryByText('UnitNaviagtion');
|
||||
|
||||
expect(nextButton).toBeNull();
|
||||
});
|
||||
|
||||
it('renders unit navigation buttons when isEnabledOutlineSidebar is true', () => {
|
||||
const props = { ...defaultProps, isEnabledOutlineSidebar: true };
|
||||
it('renders unit navigation buttons', () => {
|
||||
const props = { ...defaultProps };
|
||||
renderComponent(props);
|
||||
|
||||
const nextButton = screen.getByText('UnitNaviagtion');
|
||||
|
||||
expect(nextButton).toBeVisible();
|
||||
});
|
||||
|
||||
// Test for accessibility compliance: unit title must be an h1 (heading level 1) as the page's primary heading
|
||||
// for screen reader and accessibility compliance.
|
||||
// See: https://edx.readthedocs.io/projects/open-edx-building-and-running-a-course/en/latest/course_components/create_html_component.html#the-visual-editor
|
||||
// JIRA: https://2u-internal.atlassian.net/browse/AU-2135
|
||||
it('renders unit title as h1 heading for accessibility', () => {
|
||||
renderComponent(defaultProps);
|
||||
|
||||
const unitTitle = screen.getByRole('heading', { level: 1 });
|
||||
|
||||
expect(unitTitle).toBeInTheDocument();
|
||||
expect(unitTitle.tagName).toBe('H1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('UnitSuspense', () => {
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
import { Locked } from '@openedx/paragon/icons';
|
||||
import SidebarContext from '../../sidebar/SidebarContext';
|
||||
import messages from './messages';
|
||||
import certificateLocked from '../../../../generic/assets/edX_locked_certificate.png';
|
||||
import certificateLocked from '../../../../generic/assets/openedx_locked_certificate.png';
|
||||
import { useModel } from '../../../../generic/model-store';
|
||||
import { UpgradeButton } from '../../../../generic/upgrade-button';
|
||||
import {
|
||||
|
||||
@@ -100,13 +100,13 @@ const SequenceNavigation = ({
|
||||
);
|
||||
};
|
||||
|
||||
return sequenceStatus === LOADED && (
|
||||
return sequenceStatus === LOADED ? (
|
||||
<nav id="courseware-sequence-navigation" data-testid="courseware-sequence-navigation" className={classNames('sequence-navigation', className, { 'mr-2': shouldDisplayNotificationTriggerInSequence })}>
|
||||
{renderPreviousButton()}
|
||||
{renderUnitButtons()}
|
||||
{renderNextButton()}
|
||||
</nav>
|
||||
);
|
||||
) : null;
|
||||
};
|
||||
|
||||
SequenceNavigation.propTypes = {
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
import { breakpoints, useWindowSize } from '@openedx/paragon';
|
||||
import PropTypes from 'prop-types';
|
||||
import { useSelector } from 'react-redux';
|
||||
import {
|
||||
useEffect, useState, useMemo, useCallback,
|
||||
} from 'react';
|
||||
|
||||
import { useModel } from '@src/generic/model-store';
|
||||
import { getLocalStorage, setLocalStorage } from '@src/data/localStorage';
|
||||
import { getCoursewareOutlineSidebarSettings } from '../../data/selectors';
|
||||
|
||||
import * as discussionsSidebar from './sidebars/discussions';
|
||||
import * as notificationsSidebar from './sidebars/notifications';
|
||||
@@ -25,11 +23,10 @@ const SidebarProvider = ({
|
||||
const shouldDisplayFullScreen = useWindowSize().width < breakpoints.extraLarge.minWidth;
|
||||
const shouldDisplaySidebarOpen = useWindowSize().width > breakpoints.extraLarge.minWidth;
|
||||
const query = new URLSearchParams(window.location.search);
|
||||
const { alwaysOpenAuxiliarySidebar } = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const isInitiallySidebarOpen = shouldDisplaySidebarOpen || query.get('sidebar') === 'true';
|
||||
|
||||
let initialSidebar = shouldDisplayFullScreen ? getLocalStorage(`sidebar.${courseId}`) : null;
|
||||
if (!shouldDisplayFullScreen && isInitiallySidebarOpen && alwaysOpenAuxiliarySidebar) {
|
||||
if (!shouldDisplayFullScreen && isInitiallySidebarOpen) {
|
||||
initialSidebar = isUnitHasDiscussionTopics
|
||||
? SIDEBARS[discussionsSidebar.ID].ID
|
||||
: verifiedMode && SIDEBARS[notificationsSidebar.ID].ID;
|
||||
|
||||
@@ -23,7 +23,6 @@ const CourseOutlineTray = () => {
|
||||
const {
|
||||
courseId,
|
||||
unitId,
|
||||
isEnabledSidebar,
|
||||
currentSidebar,
|
||||
handleToggleCollapse,
|
||||
isActiveEntranceExam,
|
||||
@@ -77,7 +76,7 @@ const CourseOutlineTray = () => {
|
||||
</div>
|
||||
);
|
||||
|
||||
if (!isEnabledSidebar || isActiveEntranceExam || currentSidebar !== ID) {
|
||||
if (isActiveEntranceExam || currentSidebar !== ID) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -67,15 +67,6 @@ describe('<CourseOutlineTray />', () => {
|
||||
expect(screen.queryByRole('button', { name: 'Course outline' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('doesn\'t render when outline sidebar is disabled', async () => {
|
||||
await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: false } });
|
||||
renderWithProvider();
|
||||
|
||||
await expect(screen.queryByText(messages.loading.defaultMessage)).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: section.title })).not.toBeInTheDocument();
|
||||
expect(screen.queryByRole('button', { name: messages.toggleCourseOutlineTrigger.defaultMessage })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders correctly when course outline is loaded', async () => {
|
||||
await initTestStore();
|
||||
renderWithProvider();
|
||||
|
||||
@@ -15,13 +15,12 @@ const CourseOutlineTrigger = ({ isMobileView }) => {
|
||||
shouldDisplayFullScreen,
|
||||
handleToggleCollapse,
|
||||
isActiveEntranceExam,
|
||||
isEnabledSidebar,
|
||||
} = useCourseOutlineSidebar();
|
||||
|
||||
const isDisplayForDesktopView = !isMobileView && !shouldDisplayFullScreen && currentSidebar !== ID;
|
||||
const isDisplayForMobileView = isMobileView && shouldDisplayFullScreen;
|
||||
|
||||
if ((!isDisplayForDesktopView && !isDisplayForMobileView) || !isEnabledSidebar || isActiveEntranceExam) {
|
||||
if ((!isDisplayForDesktopView && !isDisplayForMobileView) || isActiveEntranceExam) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('<CourseOutlineTrigger />', () => {
|
||||
it('renders correctly for desktop when sidebar is enabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockToggleSidebar = jest.fn();
|
||||
await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
|
||||
await initTestStore();
|
||||
renderWithProvider({ toggleSidebar: mockToggleSidebar }, { isMobileView: false });
|
||||
|
||||
const toggleButton = await screen.getByRole('button', {
|
||||
@@ -62,7 +62,7 @@ describe('<CourseOutlineTrigger />', () => {
|
||||
it('renders correctly for mobile when sidebar is enabled', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockToggleSidebar = jest.fn();
|
||||
await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
|
||||
await initTestStore();
|
||||
renderWithProvider({
|
||||
toggleSidebar: mockToggleSidebar,
|
||||
shouldDisplayFullScreen: true,
|
||||
@@ -82,7 +82,7 @@ describe('<CourseOutlineTrigger />', () => {
|
||||
it('changes current sidebar value on click', async () => {
|
||||
const user = userEvent.setup();
|
||||
const mockToggleSidebar = jest.fn();
|
||||
await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: true } });
|
||||
await initTestStore();
|
||||
renderWithProvider({
|
||||
toggleSidebar: mockToggleSidebar,
|
||||
shouldDisplayFullScreen: true,
|
||||
@@ -99,14 +99,4 @@ describe('<CourseOutlineTrigger />', () => {
|
||||
expect(mockToggleSidebar).toHaveBeenCalledTimes(1);
|
||||
expect(mockToggleSidebar).toHaveBeenCalledWith(null);
|
||||
});
|
||||
|
||||
it('does not render when isEnabled is false', async () => {
|
||||
await initTestStore({ enableNavigationSidebar: { enable_navigation_sidebar: false } });
|
||||
renderWithProvider({}, { isMobileView: false });
|
||||
|
||||
const toggleButton = await screen.queryByRole('button', {
|
||||
name: messages.toggleCourseOutlineTrigger.defaultMessage,
|
||||
});
|
||||
expect(toggleButton).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -26,7 +26,6 @@ export const useCourseOutlineSidebar = () => {
|
||||
const dispatch = useDispatch();
|
||||
const isCollapsedOutlineSidebar = window.sessionStorage.getItem('hideCourseOutlineSidebar');
|
||||
const {
|
||||
enableNavigationSidebar: isEnabledSidebar,
|
||||
enableCompletionTracking: isEnabledCompletionTracking,
|
||||
} = useSelector(getCoursewareOutlineSidebarSettings);
|
||||
const courseOutlineShouldUpdate = useSelector(getCourseOutlineShouldUpdate);
|
||||
@@ -48,7 +47,7 @@ export const useCourseOutlineSidebar = () => {
|
||||
shouldDisplayFullScreen,
|
||||
} = useContext(SidebarContext);
|
||||
|
||||
const isOpenSidebar = !initialSidebar && isEnabledSidebar && !isCollapsedOutlineSidebar;
|
||||
const isOpenSidebar = !initialSidebar && !isCollapsedOutlineSidebar;
|
||||
const [isOpen, setIsOpen] = useState(true);
|
||||
|
||||
const {
|
||||
@@ -110,10 +109,10 @@ export const useCourseOutlineSidebar = () => {
|
||||
}, [initialSidebar, unitId]);
|
||||
|
||||
useEffect(() => {
|
||||
if ((isEnabledSidebar && courseOutlineStatus !== LOADED) || courseOutlineShouldUpdate) {
|
||||
if (courseOutlineStatus !== LOADED || courseOutlineShouldUpdate) {
|
||||
dispatch(getCourseOutlineStructure(courseId));
|
||||
}
|
||||
}, [courseId, isEnabledSidebar, courseOutlineShouldUpdate]);
|
||||
}, [courseId, courseOutlineShouldUpdate]);
|
||||
|
||||
// Collapse sidebar if screen resized to a width that displays the sidebar automatically
|
||||
useLayoutEffect(() => {
|
||||
@@ -135,7 +134,6 @@ export const useCourseOutlineSidebar = () => {
|
||||
unitId,
|
||||
currentSidebar,
|
||||
shouldDisplayFullScreen,
|
||||
isEnabledSidebar,
|
||||
isEnabledCompletionTracking,
|
||||
isOpen,
|
||||
setIsOpen,
|
||||
|
||||
@@ -104,17 +104,15 @@ export async function getCourseOutline(courseId) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get waffle flag value that enable courseware outline sidebar and always open auxiliary sidebar.
|
||||
* Get waffle flag value that enables completion tracking.
|
||||
* @param {string} courseId - The unique identifier for the course.
|
||||
* @returns {Promise<{enable_navigation_sidebar: boolean, enable_navigation_sidebar: boolean}>} - The object
|
||||
* of boolean values of enabling of the outline sidebar and is always open auxiliary sidebar.
|
||||
* @returns {Promise<{enable_completion_tracking: boolean}>} - The object
|
||||
* of boolean values of enabling of the completion tracking.
|
||||
*/
|
||||
export async function getCoursewareOutlineSidebarToggles(courseId) {
|
||||
const url = new URL(`${getConfig().LMS_BASE_URL}/courses/${courseId}/courseware-navigation-sidebar/toggles/`);
|
||||
const { data } = await getAuthenticatedHttpClient().get(url.href);
|
||||
return {
|
||||
enable_navigation_sidebar: data.enable_navigation_sidebar || false,
|
||||
always_open_auxiliary_sidebar: data.always_open_auxiliary_sidebar || false,
|
||||
enable_completion_tracking: data.enable_completion_tracking || false,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -111,8 +111,6 @@ describe('Data layer integration tests', () => {
|
||||
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
|
||||
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
|
||||
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
|
||||
enable_navigation_sidebar: true,
|
||||
always_open_auxiliary_sidebar: true,
|
||||
enable_completion_tracking: true,
|
||||
});
|
||||
|
||||
@@ -125,8 +123,6 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceStatus).toEqual('loading');
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
|
||||
enableNavigationSidebar: true,
|
||||
alwaysOpenAuxiliarySidebar: true,
|
||||
enableCompletionTracking: true,
|
||||
});
|
||||
|
||||
@@ -141,8 +137,7 @@ describe('Data layer integration tests', () => {
|
||||
axiosMock.onGet(courseUrl).reply(200, courseMetadata);
|
||||
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, simpleOutline);
|
||||
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
|
||||
enable_navigation_sidebar: false,
|
||||
always_open_auxiliary_sidebar: false,
|
||||
enable_completion_tracking: false,
|
||||
});
|
||||
|
||||
await executeThunk(thunks.fetchCourse(courseId), store.dispatch);
|
||||
@@ -154,8 +149,6 @@ describe('Data layer integration tests', () => {
|
||||
expect(state.courseware.sequenceStatus).toEqual('loading');
|
||||
expect(state.courseware.sequenceId).toEqual(null);
|
||||
expect(state.courseware.coursewareOutlineSidebarSettings).toEqual({
|
||||
enableNavigationSidebar: false,
|
||||
alwaysOpenAuxiliarySidebar: false,
|
||||
enableCompletionTracking: false,
|
||||
});
|
||||
|
||||
|
||||
@@ -88,12 +88,10 @@ export function fetchCourse(courseId) {
|
||||
|
||||
if (fetchedCoursewareOutlineSidebarTogglesResult) {
|
||||
const {
|
||||
enable_navigation_sidebar: enableNavigationSidebar,
|
||||
always_open_auxiliary_sidebar: alwaysOpenAuxiliarySidebar,
|
||||
enable_completion_tracking: enableCompletionTracking,
|
||||
} = coursewareOutlineSidebarTogglesResult.value;
|
||||
dispatch(setCoursewareOutlineSidebarToggles(
|
||||
{ enableNavigationSidebar, alwaysOpenAuxiliarySidebar, enableCompletionTracking },
|
||||
{ enableCompletionTracking },
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
|
||||
<div>
|
||||
PageWrap: {
|
||||
"children": [
|
||||
" ",
|
||||
[
|
||||
" ",
|
||||
[],
|
||||
" "
|
||||
],
|
||||
" "
|
||||
]
|
||||
}
|
||||
</div>
|
||||
`;
|
||||
@@ -62,11 +62,10 @@ describe('DecodePageRoute', () => {
|
||||
const props = matchPath({
|
||||
path: '/course/:courseId/home',
|
||||
}, `/course/${decodedCourseId}/home`);
|
||||
const { container } = renderPage(props);
|
||||
renderPage(props);
|
||||
|
||||
expect(props.pathname).toContain(decodedCourseId);
|
||||
expect(mockNavigate).not.toHaveBeenCalled();
|
||||
expect(container).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it('should decode the url and replace the history if necessary', () => {
|
||||
|
||||
|
Before Width: | Height: | Size: 8.1 KiB |
|
Before Width: | Height: | Size: 6.5 KiB |
BIN
src/generic/assets/openedx_certificate.png
Normal file
|
After Width: | Height: | Size: 8.3 KiB |
BIN
src/generic/assets/openedx_locked_certificate.png
Normal file
|
After Width: | Height: | Size: 6.7 KiB |
@@ -196,7 +196,6 @@ initialize({
|
||||
PRIVACY_POLICY_URL: process.env.PRIVACY_POLICY_URL || null,
|
||||
SHOW_UNGRADED_ASSIGNMENT_PROGRESS: process.env.SHOW_UNGRADED_ASSIGNMENT_PROGRESS || false,
|
||||
ENABLE_XPERT_AUDIT: process.env.ENABLE_XPERT_AUDIT || false,
|
||||
FEATURE_ENABLE_CHAT_V2_ENDPOINT: process.env.FEATURE_ENABLE_CHAT_V2_ENDPOINT || false,
|
||||
}, 'LearnerAppConfig');
|
||||
},
|
||||
},
|
||||
|
||||
@@ -302,11 +302,6 @@
|
||||
padding-left: 40px;
|
||||
padding-right: 40px;
|
||||
}
|
||||
|
||||
// Unit title is styled as an H3
|
||||
.unit-title {
|
||||
font-size: var(--pgn-typography-font-size-h3-base);
|
||||
}
|
||||
}
|
||||
|
||||
.unit-iframe-wrapper {
|
||||
@@ -372,22 +367,19 @@
|
||||
// window (retaining padding around the edge). Bootstrap modals don't have a full-screen
|
||||
// size like this. Because of the hack below around react-focus-on's div, it would be better long-term to pull this into Paragon and perhaps call it "modal-full" or something like that.
|
||||
.modal-lti {
|
||||
height: 80vh;
|
||||
height: 100%;
|
||||
max-width: 100% !important;
|
||||
|
||||
// I don't like this. We need to set a height of 100% on a div created by react-focus-on, a
|
||||
// package we use in our Modal. That div has no class name or ID, so instead we're uniquely
|
||||
// identifying it by based on a unique attribute it has which its siblings don't share.
|
||||
> div[data-focus-lock-disabled="false"] {
|
||||
height: 80vh;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
// Along with setting the height of modal-content's parent div from react-focus-on, we need to
|
||||
// set modal-content's height as well to get the modal to expand to full-screen height.
|
||||
.modal-content {
|
||||
height: 80vh;
|
||||
}
|
||||
.pgn__modal-body-content {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,15 +69,11 @@ describe('app registry', () => {
|
||||
const callArgs = subscribe.mock.calls[0];
|
||||
expect(callArgs[0]).toEqual(APP_READY);
|
||||
callArgs[1]();
|
||||
const [rendered] = mockRender.mock.calls[0];
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
|
||||
test('subscribe: APP_INIT_ERROR.', () => {
|
||||
const callArgs = subscribe.mock.calls[1];
|
||||
expect(callArgs[0]).toEqual(APP_INIT_ERROR);
|
||||
const error = { message: 'test-error-message' };
|
||||
callArgs[1](error);
|
||||
const [rendered] = mockRender.mock.calls[0];
|
||||
expect(rendered).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
# Content iFrame Error Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.learning.content_iframe_error.v1`
|
||||
|
||||
### Parameters: `courseId`
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to replace/modify the content iframe error page.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the error page with emojis.
|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.content_iframe_error.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_error_page',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({courseId}) => (
|
||||
<h1>🚨🤖💥</h1>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { ErrorPage } from '@edx/frontend-platform/react';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
}
|
||||
|
||||
export const ContentIFrameErrorSlot : React.FC<Props> = ({ courseId }: Props) => (
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.learning.content_iframe_error.v1"
|
||||
pluginProps={{ courseId }}
|
||||
>
|
||||
<ErrorPage />
|
||||
</PluginSlot>
|
||||
);
|
||||
@@ -1,4 +1,4 @@
|
||||
# Content iframe Loader Slot
|
||||
# Content IFrame Loader Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.learning.content_iframe_loader.v1`
|
||||
|
||||
@@ -6,5 +6,51 @@
|
||||
* `content_iframe_loader_slot`
|
||||
|
||||
### Props:
|
||||
* `courseId`
|
||||
* `defaultLoaderComponent`
|
||||
* `courseId` - String identifier for the current course
|
||||
* `defaultLoaderComponent` - React component used as the default loading indicator
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to customize the loading indicator displayed while course content is being loaded in an iframe. It appears when content is loading but hasn't fully rendered yet, providing a customizable loading experience for learners.
|
||||
|
||||
The default implementation shows a `PageLoading` component with a screen reader message.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the default loading spinner with a custom loading component that shows the course ID and a custom message.
|
||||
|
||||

|
||||
|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.content_iframe_loader.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widgetId: 'default_contents',
|
||||
widget: {
|
||||
id: 'custom_iframe_loader',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({ courseId, defaultLoaderComponent }) => (
|
||||
<div style={{ textAlign: 'center', padding: '2rem' }}>
|
||||
<h3>Loading course content...</h3>
|
||||
<p>Course: {courseId}</p>
|
||||
<div style={{ margin: '1rem 0' }}>
|
||||
{defaultLoaderComponent}
|
||||
</div>
|
||||
<p>Please wait while we prepare your learning experience</p>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 80 KiB |
@@ -14,6 +14,44 @@ This slot is used to replace/modify/hide the course breadcrumbs.
|
||||
### Default content
|
||||

|
||||
|
||||
### Replace with default breadcrumbs component
|
||||
You can also inject the default `CourseBreadcrumbs` component explicitly using the slot system, for example to wrap or style it differently.
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import CourseBreadcrumbs from './src/courseware/course/breadcrumbs';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.course_breadcrumbs.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'default_breadcrumbs_component',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({ courseId, sectionId, sequenceId, isStaff, unitId }) => (
|
||||
<CourseBreadcrumbs
|
||||
courseId={courseId}
|
||||
sectionId={sectionId}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replaced with custom component
|
||||

|
||||
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from 'react';
|
||||
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import CourseBreadcrumbs from '../../courseware/course/breadcrumbs';
|
||||
|
||||
interface Props {
|
||||
courseId: string;
|
||||
sectionId?: string;
|
||||
@@ -21,13 +19,12 @@ export const CourseBreadcrumbsSlot : React.FC<Props> = ({
|
||||
slotOptions={{
|
||||
mergeProps: true,
|
||||
}}
|
||||
>
|
||||
<CourseBreadcrumbs
|
||||
courseId={courseId}
|
||||
sectionId={sectionId}
|
||||
sequenceId={sequenceId}
|
||||
isStaff={isStaff}
|
||||
unitId={unitId}
|
||||
/>
|
||||
</PluginSlot>
|
||||
pluginProps={{
|
||||
courseId,
|
||||
sectionId,
|
||||
sequenceId,
|
||||
unitId,
|
||||
isStaff,
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 55 KiB |
|
After Width: | Height: | Size: 46 KiB |
@@ -6,5 +6,55 @@
|
||||
* `outline_tab_notifications_slot`
|
||||
|
||||
### Props:
|
||||
* `courseId`
|
||||
* `model`
|
||||
* `courseId` - String identifier for the current course
|
||||
* `model` - String indicating the context model (set to 'outline')
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to add custom notification components to the course outline tab sidebar. It appears in the right sidebar of the course outline/home tab, positioned between the Course Tools widget and the Course Dates widget.
|
||||
|
||||
The slot provides a flexible way to inject custom notifications, announcements, or informational components that are contextually relevant to the course outline view.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will add a custom notification component to the course outline tab sidebar.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.course_outline_tab_notifications.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_outline_notification',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({ courseId, model }) => (
|
||||
<div className="card mb-3">
|
||||
<div className="card-body">
|
||||
<h5 className="card-title">📢 Course Announcement</h5>
|
||||
<p className="card-text">
|
||||
Important updates for course {courseId}
|
||||
</p>
|
||||
<p className="text-muted small">
|
||||
Context: {model}
|
||||
</p>
|
||||
<button className="btn btn-primary btn-sm">
|
||||
View Details
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 135 KiB |
@@ -6,4 +6,65 @@
|
||||
* `gated_unit_content_message_slot`
|
||||
|
||||
### Props:
|
||||
* `courseId`
|
||||
* `courseId` - String identifier for the current course
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to customize the message displayed when course content is gated or locked for learners who haven't upgraded to a verified track. It appears when a unit contains content that requires a paid enrollment (such as graded assignments) and the learner is on the audit track.
|
||||
|
||||
The default implementation shows a `LockPaywall` component that displays an upgrade message with benefits of upgrading, including access to graded assignments, certificates, and full course features.
|
||||
|
||||
This slot is conditionally rendered only when `contentTypeGatingEnabled` is true and the unit `containsContentTypeGatedContent`.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will replace the default paywall message with a custom gated content component.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.gated_unit_content_message.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widgetId: 'default_contents',
|
||||
widget: {
|
||||
id: 'custom_gated_message',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({ courseId }) => (
|
||||
<div className="alert alert-warning" role="alert">
|
||||
<div className="d-flex align-items-center mb-3">
|
||||
<i className="fa fa-lock fa-2x me-3" aria-hidden="true"></i>
|
||||
<div>
|
||||
<h4 className="alert-heading mb-1">Premium Content</h4>
|
||||
<p className="mb-0">This content is available to verified learners only.</p>
|
||||
</div>
|
||||
</div>
|
||||
<hr />
|
||||
<p className="mb-3">
|
||||
Upgrade your enrollment for course {courseId} to access:
|
||||
</p>
|
||||
<ul className="mb-3">
|
||||
<li>✅ Graded assignments and quizzes</li>
|
||||
<li>🏆 Verified certificate upon completion</li>
|
||||
<li>💬 Full discussion forum access</li>
|
||||
<li>📱 Mobile app offline access</li>
|
||||
</ul>
|
||||
<button className="btn btn-success">
|
||||
Upgrade Now
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 74 KiB |
@@ -1,28 +0,0 @@
|
||||
# Learner Tools Slot
|
||||
|
||||
### Slot ID: `org.openedx.frontend.learning.learner_tools.v1`
|
||||
|
||||
### Slot ID Aliases
|
||||
* `learner_tools_slot`
|
||||
|
||||
### Description
|
||||
This plugin slot provides a location for learner-facing tools and features to be displayed during course content navigation. The slot is rendered via a React portal to `document.body` to ensure proper positioning and stacking context.
|
||||
|
||||
### Props:
|
||||
* `courseId` - The unique identifier for the current course
|
||||
* `unitId` - The unique identifier for the current unit/vertical being viewed
|
||||
* `userId` - The authenticated user's ID (automatically retrieved from auth context)
|
||||
* `isStaff` - Boolean indicating whether the user has staff/instructor privileges
|
||||
* `enrollmentMode` - The user's enrollment mode (e.g., 'audit', 'verified', 'honor', etc.)
|
||||
|
||||
### Usage
|
||||
Plugins registered to this slot can use the provided context to:
|
||||
- Display course-specific tools based on courseId and unitId
|
||||
- Show different features based on user's enrollment mode
|
||||
- Provide staff-only functionality when isStaff is true
|
||||
- Query additional data from Redux store or backend APIs as needed
|
||||
|
||||
### Notes
|
||||
- Returns `null` if user is not authenticated
|
||||
- Plugins should manage their own feature flag checks and requirements
|
||||
- The slot uses a portal to render to `document.body` for flexible positioning
|
||||
@@ -1,47 +0,0 @@
|
||||
import { createPortal } from 'react-dom';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
|
||||
|
||||
export const LearnerToolsSlot = ({
|
||||
enrollmentMode = null,
|
||||
isStaff,
|
||||
courseId,
|
||||
unitId,
|
||||
}) => {
|
||||
const authenticatedUser = getAuthenticatedUser();
|
||||
|
||||
// Return null if user is not authenticated to avoid destructuring errors
|
||||
if (!authenticatedUser) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { userId } = authenticatedUser;
|
||||
|
||||
// Provide minimal, generic context - no feature-specific flags
|
||||
const pluginContext = {
|
||||
courseId,
|
||||
unitId,
|
||||
userId,
|
||||
isStaff,
|
||||
enrollmentMode,
|
||||
};
|
||||
|
||||
// Use generic plugin slot ID (location-based, not feature-specific)
|
||||
// Plugins will query their own requirements from Redux/config
|
||||
return createPortal(
|
||||
<PluginSlot
|
||||
id="org.openedx.frontend.learning.learner_tools.v1"
|
||||
idAliases={['learner_tools_slot']}
|
||||
pluginProps={pluginContext}
|
||||
/>,
|
||||
document.body,
|
||||
);
|
||||
};
|
||||
|
||||
LearnerToolsSlot.propTypes = {
|
||||
isStaff: PropTypes.bool.isRequired,
|
||||
enrollmentMode: PropTypes.string,
|
||||
courseId: PropTypes.string.isRequired,
|
||||
unitId: PropTypes.string.isRequired,
|
||||
};
|
||||
@@ -1,104 +0,0 @@
|
||||
import React from 'react';
|
||||
import { render } from '@testing-library/react';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
import * as auth from '@edx/frontend-platform/auth';
|
||||
|
||||
import { LearnerToolsSlot } from './index';
|
||||
|
||||
jest.mock('@openedx/frontend-plugin-framework', () => ({
|
||||
PluginSlot: jest.fn(() => <div data-testid="plugin-slot">Plugin Slot</div>),
|
||||
}));
|
||||
|
||||
jest.mock('@edx/frontend-platform/auth', () => ({
|
||||
getAuthenticatedUser: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('LearnerToolsSlot', () => {
|
||||
const defaultProps = {
|
||||
courseId: 'course-v1:edX+DemoX+Demo_Course',
|
||||
unitId: 'block-v1:edX+DemoX+Demo_Course+type@vertical+block@unit1',
|
||||
isStaff: false,
|
||||
enrollmentMode: 'verified',
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
// Mock document.body for createPortal
|
||||
document.body.innerHTML = '<div id="root"></div>';
|
||||
});
|
||||
|
||||
it('renders PluginSlot with correct props when user is authenticated', () => {
|
||||
const mockUser = { userId: 123, username: 'testuser' };
|
||||
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||
|
||||
render(<LearnerToolsSlot {...defaultProps} />);
|
||||
|
||||
expect(PluginSlot).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
id: 'org.openedx.frontend.learning.learner_tools.v1',
|
||||
idAliases: ['learner_tools_slot'],
|
||||
pluginProps: {
|
||||
courseId: defaultProps.courseId,
|
||||
unitId: defaultProps.unitId,
|
||||
userId: mockUser.userId,
|
||||
isStaff: defaultProps.isStaff,
|
||||
enrollmentMode: defaultProps.enrollmentMode,
|
||||
},
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('returns null when user is not authenticated', () => {
|
||||
auth.getAuthenticatedUser.mockReturnValue(null);
|
||||
|
||||
const { container } = render(<LearnerToolsSlot {...defaultProps} />);
|
||||
|
||||
expect(container.firstChild).toBeNull();
|
||||
expect(PluginSlot).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('uses default null for enrollmentMode when not provided', () => {
|
||||
const mockUser = { userId: 456, username: 'testuser2' };
|
||||
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||
|
||||
const { enrollmentMode, ...propsWithoutEnrollmentMode } = defaultProps;
|
||||
|
||||
render(<LearnerToolsSlot {...propsWithoutEnrollmentMode} />);
|
||||
|
||||
expect(PluginSlot).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginProps: expect.objectContaining({
|
||||
enrollmentMode: null,
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('passes isStaff=true correctly', () => {
|
||||
const mockUser = { userId: 789, username: 'staffuser' };
|
||||
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||
|
||||
render(<LearnerToolsSlot {...defaultProps} isStaff />);
|
||||
|
||||
expect(PluginSlot).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
pluginProps: expect.objectContaining({
|
||||
isStaff: true,
|
||||
}),
|
||||
}),
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
it('renders to document.body via portal', () => {
|
||||
const mockUser = { userId: 999, username: 'portaluser' };
|
||||
auth.getAuthenticatedUser.mockReturnValue(mockUser);
|
||||
|
||||
render(<LearnerToolsSlot {...defaultProps} />);
|
||||
|
||||
// The portal should render to document.body
|
||||
expect(document.body.querySelector('[data-testid="plugin-slot"]')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -6,6 +6,92 @@
|
||||
* `notification_tray_slot`
|
||||
|
||||
### Props:
|
||||
* `courseId`
|
||||
* `notificationCurrentState`
|
||||
* `setNotificationCurrentState`
|
||||
* `courseId` - String identifier for the current course
|
||||
* `model` - String indicating the context model (set to 'coursewareMeta')
|
||||
* `notificationCurrentState` - Current state of upgrade notifications (UpgradeNotificationState)
|
||||
* `setNotificationCurrentState` - Function to update the notification state
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to customize the notification tray that appears in the courseware sidebar. The notification tray displays upgrade-related notifications and alerts for learners in verified mode courses. It provides a way to show contextual notifications about course access, deadlines, and upgrade opportunities.
|
||||
|
||||
The slot is conditionally rendered only for learners in verified mode courses. For non-verified courses, a simple "no notifications" message is displayed instead.
|
||||
|
||||
The `notificationCurrentState` can be one of: `'accessLastHour'`, `'accessHoursLeft'`, `'accessDaysLeft'`, `'FPDdaysLeft'`, `'FPDLastHour'`, `'accessDateView'`, or `'PastExpirationDate'`.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will customize the notification tray with additional notification types and styling.
|
||||
|
||||

|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.notification_tray.v1': {
|
||||
plugins: [
|
||||
{
|
||||
// Insert custom notification content
|
||||
op: PLUGIN_OPERATIONS.Replace,
|
||||
widget: {
|
||||
id: 'custom_notifications',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({
|
||||
courseId,
|
||||
model,
|
||||
notificationCurrentState,
|
||||
setNotificationCurrentState
|
||||
}) => (
|
||||
<div className="p-3">
|
||||
<h5 className="mb-3">📬 Course Notifications</h5>
|
||||
|
||||
{notificationCurrentState === 'accessLastHour' && (
|
||||
<div className="alert alert-warning mb-3">
|
||||
<strong>⏰ Last Chance!</strong>
|
||||
<p className="mb-0">Your access expires in less than an hour.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{notificationCurrentState === 'accessDaysLeft' && (
|
||||
<div className="alert alert-info mb-3">
|
||||
<strong>📅 Access Reminder</strong>
|
||||
<p className="mb-0">Your course access expires in a few days.</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="notification-item p-2 mb-2 border rounded">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<span>📚 New course material available</span>
|
||||
<button
|
||||
className="btn btn-sm btn-outline-primary"
|
||||
onClick={() => setNotificationCurrentState('accessDateView')}
|
||||
>
|
||||
View
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="notification-item p-2 mb-2 border rounded">
|
||||
<div className="d-flex justify-content-between align-items-center">
|
||||
<span>🎯 Assignment due tomorrow</span>
|
||||
<span className="badge bg-warning">Due Soon</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="text-center mt-3">
|
||||
<small className="text-muted">
|
||||
Course: {courseId} | Model: {model}
|
||||
</small>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 118 KiB |
@@ -6,8 +6,125 @@
|
||||
* `notification_widget_slot`
|
||||
|
||||
### Props:
|
||||
* `courseId`
|
||||
* `model`
|
||||
* `notificationCurrentState`
|
||||
* `setNotificationCurrentState`
|
||||
* `toggleSidebar`
|
||||
* `courseId` - String identifier for the current course
|
||||
* `model` - String indicating the context model (set to 'coursewareMeta')
|
||||
* `notificationCurrentState` - Current state of upgrade notifications (UpgradeNotificationState)
|
||||
* `setNotificationCurrentState` - Function to update the notification state
|
||||
* `toggleSidebar` - Function to toggle the sidebar open/closed
|
||||
|
||||
## Description
|
||||
|
||||
This slot is used to customize the notification widget that appears in the discussions-notifications sidebar. The widget is displayed as a compact notification component that shows upgrade-related alerts and can trigger the full notification tray when clicked.
|
||||
|
||||
The widget appears in the combined discussions-notifications sidebar and is conditionally rendered based on the `hideNotificationbar` and `isNotificationbarAvailable` flags. It automatically tracks user engagement and calls `onNotificationSeen` after a 3-second timeout.
|
||||
|
||||
The `notificationCurrentState` can be one of: `'accessLastHour'`, `'accessHoursLeft'`, `'accessDaysLeft'`, `'FPDdaysLeft'`, `'FPDLastHour'`, `'accessDateView'`, or `'PastExpirationDate'`.
|
||||
|
||||
## Example
|
||||
|
||||
The following `env.config.jsx` will customize the notification widget with a more interactive design and additional functionality.
|
||||
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
const NotificationWidget = ({
|
||||
courseId,
|
||||
model,
|
||||
notificationCurrentState,
|
||||
setNotificationCurrentState,
|
||||
toggleSidebar
|
||||
}) => {
|
||||
const getNotificationContent = () => {
|
||||
switch (notificationCurrentState) {
|
||||
case 'accessLastHour':
|
||||
return {
|
||||
icon: '⚠️',
|
||||
title: 'Final Hour!',
|
||||
message: 'Access expires in less than 1 hour',
|
||||
variant: 'danger'
|
||||
};
|
||||
case 'accessHoursLeft':
|
||||
return {
|
||||
icon: '⏰',
|
||||
title: 'Expiring Soon',
|
||||
message: 'Access expires in a few hours',
|
||||
variant: 'warning'
|
||||
};
|
||||
case 'accessDaysLeft':
|
||||
return {
|
||||
icon: '📅',
|
||||
title: 'Access Reminder',
|
||||
message: 'Access expires in a few days',
|
||||
variant: 'info'
|
||||
};
|
||||
case 'FPDdaysLeft':
|
||||
case 'FPDLastHour':
|
||||
return {
|
||||
icon: '🎯',
|
||||
title: 'Upgrade Available',
|
||||
message: 'Get full access to premium features',
|
||||
variant: 'primary'
|
||||
};
|
||||
default:
|
||||
return {
|
||||
icon: '🔔',
|
||||
title: 'Notifications',
|
||||
message: 'Click to view updates',
|
||||
variant: 'secondary'
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const notification = getNotificationContent();
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`alert alert-${notification.variant} mb-0 cursor-pointer`}
|
||||
onClick={toggleSidebar}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' || e.key === ' ') {
|
||||
e.preventDefault();
|
||||
toggleSidebar();
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="d-flex align-items-center">
|
||||
<span className="me-2" style={{ fontSize: '1.2em' }}>
|
||||
{notification.icon}
|
||||
</span>
|
||||
<div className="flex-fill">
|
||||
<strong className="d-block">{notification.title}</strong>
|
||||
<small>{notification.message}</small>
|
||||
</div>
|
||||
<div className="ms-2">
|
||||
<i className="fa fa-chevron-right" aria-hidden="true"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.notification_widget.v1': {
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widgetId: 'default_contents',
|
||||
widget: {
|
||||
id: 'custom_notification_widget',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: NotificationWidget
|
||||
},
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
|
After Width: | Height: | Size: 91 KiB |
@@ -11,7 +11,6 @@
|
||||
* [`org.openedx.frontend.learning.course_outline_tab_notifications.v1`](./CourseOutlineTabNotificationsSlot/)
|
||||
* [`org.openedx.frontend.learning.course_recommendations.v1`](./CourseRecommendationsSlot/)
|
||||
* [`org.openedx.frontend.learning.gated_unit_content_message.v1`](./GatedUnitContentMessageSlot/)
|
||||
* [`org.openedx.frontend.learning.learner_tools.v1`](./LearnerToolsSlot/)
|
||||
* [`org.openedx.frontend.learning.next_unit_top_nav_trigger.v1`](./NextUnitTopNavTriggerSlot/)
|
||||
* [`org.openedx.frontend.learning.notification_tray.v1`](./NotificationTraySlot/)
|
||||
* [`org.openedx.frontend.learning.notification_widget.v1`](./NotificationWidgetSlot/)
|
||||
|
||||
@@ -16,9 +16,47 @@ This slot is used to replace/modify/hide the sequence navigation component that
|
||||
## Example
|
||||
|
||||
### Default content
|
||||

|
||||

|
||||
|
||||
### Replaced with custom component
|
||||
### Replace with default sequence navigation component
|
||||
You can also inject the default `SequenceNavigation` component explicitly using the slot system, for example to wrap or style it differently.
|
||||

|
||||
|
||||
```js
|
||||
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import { SequenceNavigation } from './src/courseware/course/sequence/sequence-navigation';
|
||||
|
||||
const config = {
|
||||
pluginSlots: {
|
||||
'org.openedx.frontend.learning.sequence_navigation.v1': {
|
||||
keepDefault: false,
|
||||
plugins: [
|
||||
{
|
||||
op: PLUGIN_OPERATIONS.Insert,
|
||||
widget: {
|
||||
id: 'custom_sequence_navigation',
|
||||
type: DIRECT_PLUGIN,
|
||||
RenderWidget: ({ sequenceId, unitId, nextHandler, onNavigate, previousHandler }) => (
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
nextHandler={nextHandler}
|
||||
onNavigate={onNavigate}
|
||||
previousHandler={previousHandler}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
```
|
||||
|
||||
### Replaced with a custom component
|
||||

|
||||
|
||||
The following `env.config.jsx` will replace the sequence navigation with a custom implementation that uses all available props.
|
||||
|
||||
@@ -2,8 +2,6 @@ import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { PluginSlot } from '@openedx/frontend-plugin-framework';
|
||||
|
||||
import { SequenceNavigation } from '../../courseware/course/sequence/sequence-navigation';
|
||||
|
||||
const SequenceNavigationSlot = ({
|
||||
sequenceId,
|
||||
unitId,
|
||||
@@ -23,15 +21,7 @@ const SequenceNavigationSlot = ({
|
||||
onNavigate,
|
||||
previousHandler,
|
||||
}}
|
||||
>
|
||||
<SequenceNavigation
|
||||
sequenceId={sequenceId}
|
||||
unitId={unitId}
|
||||
nextHandler={nextHandler}
|
||||
onNavigate={onNavigate}
|
||||
previousHandler={previousHandler}
|
||||
/>
|
||||
</PluginSlot>
|
||||
/>
|
||||
);
|
||||
|
||||
SequenceNavigationSlot.propTypes = {
|
||||
|
||||