diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml
index ed4ddaec06..284b12f60e 100644
--- a/.github/workflows/validate.yml
+++ b/.github/workflows/validate.yml
@@ -10,7 +10,7 @@ jobs:
tests:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v4
+ - uses: actions/checkout@v5
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
@@ -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:
diff --git a/.nvmrc b/.nvmrc
index 209e3ef4b6..a45fd52cc5 100644
--- a/.nvmrc
+++ b/.nvmrc
@@ -1 +1 @@
-20
+24
diff --git a/package-lock.json b/package-lock.json
index fdad0ef667..fc977b0683 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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-component-header": "^8.0.0",
"@edx/frontend-lib-learning-assistant": "^2.24.0",
"@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"
@@ -413,9 +412,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"
@@ -2228,16 +2227,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 +2288,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,87 +2325,53 @@
"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": {
@@ -2538,9 +2500,9 @@
}
},
"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",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
@@ -2576,6 +2538,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 +2564,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 +2585,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": {
@@ -3306,6 +3226,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",
@@ -4010,9 +4348,9 @@
}
},
"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",
"dependencies": {
"@babel/cli": "7.24.8",
@@ -4058,7 +4396,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 +4404,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",
@@ -4201,9 +4539,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"
@@ -4350,9 +4688,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"
@@ -5546,7 +5884,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 +5905,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 +5926,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 +5940,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,7 +6043,9 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -7569,12 +7915,6 @@
"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",
@@ -7830,82 +8170,10 @@
}
},
"node_modules/balanced-match": {
- "version": "1.0.2",
- "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
- "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
- }
- }
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
+ "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "license": "MIT"
},
"node_modules/base64-js": {
"version": "1.5.1",
@@ -8322,9 +8590,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 +8833,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 +9350,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 +9580,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 +9866,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 +9880,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,7 +10194,9 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -10237,6 +10466,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"
@@ -11522,15 +11752,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 +11874,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 +12480,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 +12679,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",
@@ -13279,16 +13482,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 +13618,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"
@@ -15377,9 +15581,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,7 +16197,9 @@
"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",
+ "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -16731,22 +16937,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 +17022,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 +17091,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 +17143,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",
@@ -18817,12 +18976,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 +19040,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,7 +19078,9 @@
"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",
+ "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -18996,7 +19094,9 @@
"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",
+ "peer": true,
"engines": {
"node": ">=10"
},
@@ -19008,7 +19108,9 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
- "license": "MIT"
+ "dev": true,
+ "license": "MIT",
+ "peer": true
},
"node_modules/process": {
"version": "0.11.10",
@@ -19120,6 +19222,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,30 +19443,6 @@
"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",
@@ -19968,19 +20047,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 +20108,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 +20260,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",
@@ -20623,9 +20670,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",
@@ -21199,26 +21246,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 +21296,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 +21419,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 +21759,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 +21961,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"
@@ -22008,9 +22011,9 @@
}
},
"node_modules/style-dictionary/node_modules/chalk": {
- "version": "5.4.1",
- "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz",
- "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==",
+ "version": "5.6.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz",
+ "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==",
"license": "MIT",
"engines": {
"node": "^12.17.0 || ^14.13 || >=16.0.0"
@@ -22246,31 +22249,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 +22369,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",
@@ -22835,18 +22804,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",
diff --git a/package.json b/package.json
index 49e01aee27..b1142f364c 100644
--- a/package.json
+++ b/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-component-header": "^8.0.0",
"@edx/frontend-lib-learning-assistant": "^2.24.0",
"@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",
diff --git a/src/__snapshots__/index.test.jsx.snap b/src/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index f0b7c6ca38..0000000000
--- a/src/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -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`] = `
-
-
-
-`;
-
-exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
-
-
-
-
-
-
-
-
-
-
-
-
-
- }
- path="*"
- />
-
-
-
- }
- path="/goal-unsubscribe/:token"
- />
-
-
-
- }
- path="/redirect/*"
- />
-
-
-
- }
- path="/preferences-unsubscribe/:userToken/:updatePatch?"
- />
-
-
-
- }
- path="/course/:courseId/access-denied"
- />
-
-
-
-
-
- }
- path="/course/:courseId/home"
- />
-
-
-
-
-
- }
- path="/course/:courseId/live"
- />
-
-
-
-
-
- }
- path="/course/:courseId/dates"
- />
-
-
-
-
-
- }
- path="/course/:courseId/discussion/:path/*"
- />
-
-
-
-
-
- }
- path="/course/:courseId/progress/:targetUserId/"
- />
-
-
-
-
-
- }
- path="/course/:courseId/progress"
- />
-
-
-
-
-
- }
- path="/course/:courseId/course-end"
- />
-
-
-
- }
- path="/course/:courseId/:sequenceId/:unitId"
- />
-
-
-
- }
- path="/course/:courseId/:sequenceId"
- />
-
-
-
- }
- path="/course/:courseId"
- />
-
-
-
- }
- path="/preview/course/:courseId/:sequenceId/:unitId"
- />
-
-
-
- }
- path="/preview/course/:courseId/:sequenceId"
- />
-
-
-
-
-
-
-
-`;
diff --git a/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx b/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx
index 7ef5909a15..0a17a4f37f 100644
--- a/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx
+++ b/src/course-home/courseware-search/CoursewareSearchEmpty.test.jsx
@@ -5,6 +5,7 @@ import {
screen,
} from '../../setupTest';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
+import messages from './messages';
function renderComponent() {
const { container } = render( );
@@ -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');
});
});
diff --git a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx
index eae95083a4..bddb40efa3 100644
--- a/src/course-home/courseware-search/CoursewareSearchResults.test.jsx
+++ b/src/course-home/courseware-search/CoursewareSearchResults.test.jsx
@@ -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');
});
});
});
diff --git a/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap b/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap
deleted file mode 100644
index e87c856ea0..0000000000
--- a/src/course-home/courseware-search/__snapshots__/CoursewareSearchEmpty.test.jsx.snap
+++ /dev/null
@@ -1,10 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CoursewareSearchEmpty should match the snapshot 1`] = `
-
- No results found.
-
-`;
diff --git a/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap b/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap
deleted file mode 100644
index 12178a62ed..0000000000
--- a/src/course-home/courseware-search/__snapshots__/CoursewareSearchResults.test.jsx.snap
+++ /dev/null
@@ -1,1238 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`CoursewareSearchResults when list of results is provided should match the snapshot 1`] = `
-
-`;
diff --git a/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap b/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap
deleted file mode 100644
index 17fc65ad55..0000000000
--- a/src/course-home/courseware-search/__snapshots__/map-search-response.test.js.snap
+++ /dev/null
@@ -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,
-}
-`;
diff --git a/src/course-home/courseware-search/map-search-response.test.js b/src/course-home/courseware-search/map-search-response.test.js
index 2ad4318a9c..75da0360cc 100644
--- a/src/course-home/courseware-search/map-search-response.test.js
+++ b/src/course-home/courseware-search/map-search-response.test.js
@@ -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', () => {
diff --git a/src/course-home/data/__factories__/progressTabData.factory.js b/src/course-home/data/__factories__/progressTabData.factory.js
index 1ff83241ce..3a3508f99e 100644
--- a/src/course-home/data/__factories__/progressTabData.factory.js
+++ b/src/course-home/data/__factories__/progressTabData.factory.js
@@ -17,7 +17,21 @@ 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',
diff --git a/src/course-home/data/__snapshots__/redux.test.js.snap b/src/course-home/data/__snapshots__/redux.test.js.snap
deleted file mode 100644
index 1b0363e660..0000000000
--- a/src/course-home/data/__snapshots__/redux.test.js.snap
+++ /dev/null
@@ -1,945 +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,
- },
- "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,
- },
- "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": "",
- "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": "Welcome to this course!
",
- },
- },
- },
- "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,
- },
- "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,
- "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",
- "gradesFeatureIsFullyLocked": false,
- "gradesFeatureIsPartiallyLocked": false,
- "gradingPolicy": {
- "assignmentPolicies": [
- {
- "averageGrade": "1.0000",
- "numDroppable": 1,
- "shortLabel": "HW",
- "type": "Homework",
- "weight": 1,
- "weightedGrade": 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,
- },
-}
-`;
diff --git a/src/course-home/data/api.js b/src/course-home/data/api.js
index 7db6f503a6..e2d24401db 100644
--- a/src/course-home/data/api.js
+++ b/src/course-home/data/api.js
@@ -3,93 +3,6 @@ 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
@@ -237,11 +150,6 @@ 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.
diff --git a/src/course-home/data/redux.test.js b/src/course-home/data/redux.test.js
index 4edbf87790..2e40e38278 100644
--- a/src/course-home/data/redux.test.js
+++ b/src/course-home/data/redux.test.js
@@ -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 () => {
diff --git a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx
index 29167e5a6f..2872f80507 100644
--- a/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx
+++ b/src/course-home/outline-tab/widgets/ProctoringInfoPanel.jsx
@@ -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
)}
-
+
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
diff --git a/src/course-home/progress-tab/ProgressTab.test.jsx b/src/course-home/progress-tab/ProgressTab.test.jsx
index 361fb14235..9195823c04 100644
--- a/src/course-home/progress-tab/ProgressTab.test.jsx
+++ b/src/course-home/progress-tab/ProgressTab.test.jsx
@@ -661,29 +661,23 @@ describe('Progress Tab', () => {
expect(screen.getByText('Grade summary')).toBeInTheDocument();
});
- it('does not render Grade Summary when assignment policies are not populated', async () => {
+ it('does not render Grade Summary when assignment type grade summary is not populated', async () => {
setTabData({
- grading_policy: {
- assignment_policies: [],
- grade_range: {
- pass: 0.75,
- },
- },
- section_scores: [],
+ assignment_type_grade_summary: [],
});
await fetchAndRender();
expect(screen.queryByText('Grade summary')).not.toBeInTheDocument();
});
- it('calculates grades correctly when number of droppable assignments equals total number of assignments', async () => {
+ it('shows lock icon when all subsections of assignment type are hidden', async () => {
setTabData({
grading_policy: {
assignment_policies: [
{
- num_droppable: 2,
- num_total: 2,
- short_label: 'HW',
- type: 'Homework',
+ num_droppable: 0,
+ num_total: 1,
+ short_label: 'Final',
+ type: 'Final Exam',
weight: 1,
},
],
@@ -691,19 +685,25 @@ describe('Progress Tab', () => {
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();
- 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();
+ // Should show lock icon for grade and weighted grade
+ expect(screen.getAllByTestId('lock-icon')).toHaveLength(2);
});
- it('calculates grades correctly when number of droppable assignments is zero', async () => {
+
+ it('shows percent plus hidden grades when some subsections of assignment type are hidden', async () => {
setTabData({
grading_policy: {
assignment_policies: [
@@ -719,41 +719,36 @@ describe('Progress Tab', () => {
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,
+ 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();
- 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();
+ // 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('calculates grades correctly when number of total assignments is greater than the number of assignments created', async () => {
+
+ 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: 5, // two assignments created in the factory, but 5 are expected per Studio settings
+ num_total: 2,
short_label: 'HW',
type: 'Homework',
weight: 1,
@@ -763,41 +758,36 @@ describe('Progress Tab', () => {
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: 'Ex',
- type: 'Exam',
- weight: 0.5,
- },
- ],
- 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();
- 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();
+
+ 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();
});
it('renders override notice', async () => {
diff --git a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
index a4ac7da7b2..bc1cd92039 100644
--- a/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
+++ b/src/course-home/progress-tab/certificate-status/CertificateStatus.jsx
@@ -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',
diff --git a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx
index e075411f25..6cada4cbb4 100644
--- a/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx
+++ b/src/course-home/progress-tab/grades/course-grade/CourseGradeFooter.jsx
@@ -8,26 +8,57 @@ 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 (
+
+ {children}
+ {hasLetterGrades && (
+
+
+
+
+ )}
+
+ );
+};
+
+const NoticeRow = ({
+ wideScreen, icon, bgClass, message,
+}) => {
+ const textClass = wideScreen ? 'h4 m-0 align-bottom' : 'h5 align-bottom';
+ return (
+
+
{icon}
+
+ {message}
+
+
+ );
+};
const CourseGradeFooter = ({ passingGrade }) => {
const intl = useIntl();
const courseId = useContextId();
const {
- courseGrade: {
- isPassing,
- letterGrade,
- },
- gradingPolicy: {
- gradeRange,
- },
+ assignmentTypeGradeSummary,
+ 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;
- const hasLetterGrades = Object.keys(gradeRange).length > 1; // A pass/fail course will only have one key
+ // build footer text
let footerText = intl.formatMessage(messages.courseGradeFooterNonPassing, { passingGrade });
-
if (isPassing) {
if (hasLetterGrades) {
const minGradeRangeCutoff = gradeRange[letterGrade] * 100;
@@ -47,42 +78,63 @@ const CourseGradeFooter = ({ passingGrade }) => {
}
}
- const icon = isPassing ?
- : ;
+ const passingIcon = isPassing ? (
+
+ ) : (
+
+ );
return (
-
-
- {icon}
-
-
- {!wideScreen && (
-
- {footerText}
- {hasLetterGrades && (
-
-
-
-
- )}
-
- )}
- {wideScreen && (
-
+
+
{footerText}
- {hasLetterGrades && (
-
-
-
-
- )}
-
+
)}
-
+ />
+ {latestDueDate && (
+ }
+ 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',
+ }),
+ })}
+ />
+ )}
);
};
+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,
};
diff --git a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx
index 36ba44e926..8e1c6b2985 100644
--- a/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx
+++ b/src/course-home/progress-tab/grades/course-grade/CurrentGradeTooltip.jsx
@@ -13,6 +13,7 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
const courseId = useContextId();
const {
+ assignmentTypeGradeSummary,
courseGrade: {
isPassing,
percent,
@@ -25,6 +26,8 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
const isLocaleRtl = isRtl(getLocale());
+ const hasHiddenGrades = assignmentTypeGradeSummary.some((assignmentType) => assignmentType.hasHiddenContribution !== 'none');
+
if (isLocaleRtl) {
currentGradeDirection = currentGrade < 50 ? '-' : '';
}
@@ -56,6 +59,15 @@ const CurrentGradeTooltip = ({ tooltipClassName }) => {
>
{intl.formatMessage(messages.currentGradeLabel)}
+
+ {hasHiddenGrades ? ` + ${intl.formatMessage(messages.hiddenScoreLabel)}` : ''}
+
>
);
};
diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx
index ffc5e2c890..6066997a9f 100644
--- a/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx
+++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummary.jsx
@@ -10,14 +10,12 @@ const GradeSummary = () => {
const courseId = useContextId();
const {
- gradingPolicy: {
- assignmentPolicies,
- },
+ assignmentTypeGradeSummary,
} = useModel('progress', courseId);
const [allOfSomeAssignmentTypeIsLocked, setAllOfSomeAssignmentTypeIsLocked] = useState(false);
- if (assignmentPolicies.length === 0) {
+ if (assignmentTypeGradeSummary.length === 0) {
return null;
}
diff --git a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx
index b6e5ceafbb..44129521e1 100644
--- a/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx
+++ b/src/course-home/progress-tab/grades/grade-summary/GradeSummaryTable.jsx
@@ -2,6 +2,7 @@ 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';
@@ -16,9 +17,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
const courseId = useContextId();
const {
- gradingPolicy: {
- assignmentPolicies,
- },
+ assignmentTypeGradeSummary,
gradesFeatureIsFullyLocked,
sectionScores,
} = useModel('progress', courseId);
@@ -55,7 +54,7 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
return false;
};
- const gradeSummaryData = assignmentPolicies.map((assignment) => {
+ const gradeSummaryData = assignmentTypeGradeSummary.map((assignment) => {
const {
averageGrade,
numDroppable,
@@ -80,13 +79,24 @@ 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 =
;
+ weightedGradeDisplay =
;
+ } 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: `${getGradePercent(averageGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
- weightedGrade: { weightedGrade: `${getGradePercent(weightedGrade)}${isLocaleRtl ? '\u200f' : ''}%`, locked },
+ grade: { grade: gradeDisplay, locked },
+ weightedGrade: { weightedGrade: weightedGradeDisplay, locked },
};
});
const getAssignmentTypeCell = (value) => (
@@ -102,6 +112,16 @@ const GradeSummaryTable = ({ setAllOfSomeAssignmentTypeIsLocked }) => {
return (
<>
+
+
+ {intl.formatMessage(messages.hiddenScoreLabel)}:
+ {intl.formatMessage(messages.hiddenScoreInfoText)}
+
+
+ :
+ {` ${intl.formatMessage(messages.hiddenScoreLockInfoText)}`}
+
+
{
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 {
@@ -36,8 +21,16 @@ 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);
diff --git a/src/course-home/progress-tab/grades/messages.ts b/src/course-home/progress-tab/grades/messages.ts
index a052096c4f..2754374461 100644
--- a/src/course-home/progress-tab/grades/messages.ts
+++ b/src/course-home/progress-tab/grades/messages.ts
@@ -21,6 +21,11 @@ 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',
@@ -148,6 +153,21 @@ 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}',
diff --git a/src/course-home/progress-tab/utils.ts b/src/course-home/progress-tab/utils.ts
index 29dd42de85..aeb40f5099 100644
--- a/src/course-home/progress-tab/utils.ts
+++ b/src/course-home/progress-tab/utils.ts
@@ -5,3 +5,15 @@ 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;
+};
diff --git a/src/courseware/CoursewareContainer.test.jsx b/src/courseware/CoursewareContainer.test.jsx
index 1343473b3b..3accecd21e 100644
--- a/src/courseware/CoursewareContainer.test.jsx
+++ b/src/courseware/CoursewareContainer.test.jsx
@@ -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();
-
- const sequenceNavButtons = container.querySelectorAll('nav.sequence-navigation a, nav.sequence-navigation button');
- const sequenceNextButton = sequenceNavButtons[4];
- expect(sequenceNextButton).toHaveTextContent('Next');
- fireEvent.click(sequenceNextButton);
+ await loadContainer();
- 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', () => {
diff --git a/src/courseware/course/Course.test.jsx b/src/courseware/course/Course.test.jsx
index 90565617c0..754ff134b5 100644
--- a/src/courseware/course/Course.test.jsx
+++ b/src/courseware/course/Course.test.jsx
@@ -210,7 +210,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 +218,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 +234,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();
});
});
diff --git a/src/courseware/course/course-exit/utils.js b/src/courseware/course/course-exit/utils.js
index c989a150dd..fb17b0e2d9 100644
--- a/src/courseware/course/course-exit/utils.js
+++ b/src/courseware/course/course-exit/utils.js
@@ -17,6 +17,7 @@ const CELEBRATION_STATUSES = [
'audit_passing',
'downloadable',
'earned_but_not_available',
+ 'not_earned_but_available_date',
'honor_passing',
'requesting',
'unverified',
diff --git a/src/courseware/course/sequence/Sequence.jsx b/src/courseware/course/sequence/Sequence.jsx
index 30d51cf8b7..cda2652b2e 100644
--- a/src/courseware/course/sequence/Sequence.jsx
+++ b/src/courseware/course/sequence/Sequence.jsx
@@ -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 = ({
/>
- {!isEnabledOutlineSidebar && (
-
- {
- 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,
- }}
- />
-
- )}
+
+ {/**
+ 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.
+ */}
+
+
{unitHasLoaded && renderUnitNavigation(false)}
diff --git a/src/courseware/course/sequence/Sequence.test.jsx b/src/courseware/course/sequence/Sequence.test.jsx
index 5473e24c30..ae58bb18fc 100644
--- a/src/courseware/course/sequence/Sequence.test.jsx
+++ b/src/courseware/course/sequence/Sequence.test.jsx
@@ -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(
,
@@ -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(
,
@@ -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,
diff --git a/src/courseware/course/sequence/SequenceContent.jsx b/src/courseware/course/sequence/SequenceContent.jsx
index 905ffbf255..6caa0fce27 100644
--- a/src/courseware/course/sequence/SequenceContent.jsx
+++ b/src/courseware/course/sequence/SequenceContent.jsx
@@ -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,
};
diff --git a/src/courseware/course/sequence/SequenceContent.test.jsx b/src/courseware/course/sequence/SequenceContent.test.jsx
index a2f14490d3..e9c3a2d785 100644
--- a/src/courseware/course/sequence/SequenceContent.test.jsx
+++ b/src/courseware/course/sequence/SequenceContent.test.jsx
@@ -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(
, { wrapWithRouter: true });
+ render(
, { wrapWithRouter: true });
expect(screen.getByText('There is no content here.')).toBeInTheDocument();
});
});
diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js
index eb70e17e53..34e56c22e4 100644
--- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js
+++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.test.js
@@ -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);
- });
- testOnlySetsHeight();
- });
- describe('iframeHeight is not 0', () => {
- 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('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,46 +280,33 @@ 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.
- expect(postMessage).toHaveBeenCalledWith(
- {
- type: 'unit.visibilityStatus',
- data: {
- topPosition: 100,
- viewportHeight: 800,
- },
- },
+ // 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', () => {
- state.mockVals({ ...defaultStateVals, hasLoaded: false });
- useIFrameBehavior(props);
-
- const effects = getEffects([false, props.elementId], React);
- expect(effects).toBeNull();
+ 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', () => {
- state.mockVals({ ...defaultStateVals, hasLoaded: true });
- useIFrameBehavior(props);
+ mockState({ ...defaultStateVals, hasLoaded: true });
+ const { unmount } = renderHook(() => 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.
+ unmount(); // Call the cleanup function.
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
@@ -328,14 +317,16 @@ describe('useIFrameBehavior hook', () => {
describe('output', () => {
describe('handleIFrameLoad', () => {
it('sets and logs error if has not loaded', () => {
- hook = useIFrameBehavior(props);
- hook.handleIFrameLoad();
- expect(state.setState.showError).toHaveBeenCalledWith(true);
+ 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', () => {
- hook = useIFrameBehavior(props);
- hook.handleIFrameLoad();
+ mockState(defaultStateVals);
+ const { result } = renderHook(() => useIFrameBehavior(props));
+ result.current.handleIFrameLoad();
const eventName = 'edx.bi.error.learning.iframe_load_failed';
const eventProperties = {
unitId: props.id,
@@ -344,33 +335,72 @@ describe('useIFrameBehavior hook', () => {
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();
+ 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', () => {
- state.mockVals({ ...defaultStateVals, hasLoaded: true });
- hook = useIFrameBehavior(props);
- hook.handleIFrameLoad();
+ 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.', () => {
- hook = useIFrameBehavior(props);
- hook.handleIFrameLoad();
+ 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',
+ data: {
+ topPosition: 100,
+ viewportHeight: 800,
+ },
+ },
+ config.LMS_BASE_URL,
+ );
+ });
});
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();
});
});
});
diff --git a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts
similarity index 62%
rename from src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js
rename to src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts
index ab40436a70..4a882da321 100644
--- a/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.js
+++ b/src/courseware/course/sequence/Unit/hooks/useIFrameBehavior.ts
@@ -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
(val), // eslint-disable-line
+ hasLoaded: (val) => useState(val), // eslint-disable-line
+ showError: (val) => useState(val), // eslint-disable-line
+ windowTopOffset: (val) => useState(val), // eslint-disable-line
+} as const;
const useIFrameBehavior = ({
elementId,
@@ -31,23 +34,29 @@ const useIFrameBehavior = ({
useLoadBearingHook(id);
const dispatch = useDispatch();
-
- 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 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] = 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(() => {
diff --git a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js
index 513b0b7636..424b4ba3be 100644
--- a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js
+++ b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.js
@@ -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');
diff --git a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js
index 99e886f113..29068a1797 100644
--- a/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js
+++ b/src/courseware/course/sequence/Unit/hooks/useModalIFrameData.test.js
@@ -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,
diff --git a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js
index 960944c60f..a190b62df5 100644
--- a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js
+++ b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.js
@@ -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);
diff --git a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js
index af4aac568d..19ecf5adb1 100644
--- a/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js
+++ b/src/courseware/course/sequence/Unit/hooks/useShouldDisplayHonorCode.test.js
@@ -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);
+ });
+
+ it('should return false when graded is false', () => {
+ mockModels(false, true);
+
+ 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 true when both userNeedsIntegritySignature and graded are true', () => {
+ mockModels(true, true);
+
+ const { result } = renderHook(() => useShouldDisplayHonorCode(props));
+ expect(result.current).toBe(true);
});
});
diff --git a/src/courseware/course/sequence/Unit/index.jsx b/src/courseware/course/sequence/Unit/index.jsx
index 37eb396d88..3a9c71bc94 100644
--- a/src/courseware/course/sequence/Unit/index.jsx
+++ b/src/courseware/course/sequence/Unit/index.jsx
@@ -22,7 +22,6 @@ const Unit = ({
onLoaded,
id,
isOriginalUserStaff,
- isEnabledOutlineSidebar,
renderUnitNavigation,
}) => {
const { formatMessage } = useIntl();
@@ -48,7 +47,7 @@ const Unit = ({
return (
-
+
enabled && 'UnitNaviagtion'),
};
@@ -68,16 +67,8 @@ describe(' ', () => {
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');
diff --git a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
index 22f61c478a..81cd5a6bc7 100644
--- a/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
+++ b/src/courseware/course/sequence/sequence-navigation/SequenceNavigation.jsx
@@ -100,13 +100,13 @@ const SequenceNavigation = ({
);
};
- return sequenceStatus === LOADED && (
+ return sequenceStatus === LOADED ? (
{renderPreviousButton()}
{renderUnitButtons()}
{renderNextButton()}
- );
+ ) : null;
};
SequenceNavigation.propTypes = {
diff --git a/src/courseware/course/sidebar/SidebarContextProvider.jsx b/src/courseware/course/sidebar/SidebarContextProvider.jsx
index 05472c01dd..9b3b824d53 100644
--- a/src/courseware/course/sidebar/SidebarContextProvider.jsx
+++ b/src/courseware/course/sidebar/SidebarContextProvider.jsx
@@ -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;
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
index 8cd17712f5..8eaf5e8a0b 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.jsx
@@ -23,7 +23,6 @@ const CourseOutlineTray = () => {
const {
courseId,
unitId,
- isEnabledSidebar,
currentSidebar,
handleToggleCollapse,
isActiveEntranceExam,
@@ -77,7 +76,7 @@ const CourseOutlineTray = () => {
);
- if (!isEnabledSidebar || isActiveEntranceExam || currentSidebar !== ID) {
+ if (isActiveEntranceExam || currentSidebar !== ID) {
return null;
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
index 1f826a690a..aba41c8291 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTray.test.jsx
@@ -67,15 +67,6 @@ describe(' ', () => {
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();
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
index dfc698de3d..abccd14aed 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.jsx
@@ -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;
}
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
index cca273db71..3b931acdad 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
+++ b/src/courseware/course/sidebar/sidebars/course-outline/CourseOutlineTrigger.test.jsx
@@ -45,7 +45,7 @@ describe(' ', () => {
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(' ', () => {
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(' ', () => {
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(' ', () => {
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();
- });
});
diff --git a/src/courseware/course/sidebar/sidebars/course-outline/hooks.js b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js
index 71ed4bff63..0127d6025d 100644
--- a/src/courseware/course/sidebar/sidebars/course-outline/hooks.js
+++ b/src/courseware/course/sidebar/sidebars/course-outline/hooks.js
@@ -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,
diff --git a/src/courseware/data/api.js b/src/courseware/data/api.js
index b19dce4cdf..f7e45fae05 100644
--- a/src/courseware/data/api.js
+++ b/src/courseware/data/api.js
@@ -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,
};
}
diff --git a/src/courseware/data/redux.test.js b/src/courseware/data/redux.test.js
index 91b6e210e7..5a2c54c8fa 100644
--- a/src/courseware/data/redux.test.js
+++ b/src/courseware/data/redux.test.js
@@ -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,
});
diff --git a/src/courseware/data/thunks.js b/src/courseware/data/thunks.js
index 15d36f7251..2312cf4daa 100644
--- a/src/courseware/data/thunks.js
+++ b/src/courseware/data/thunks.js
@@ -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 },
));
}
diff --git a/src/decode-page-route/__snapshots__/index.test.jsx.snap b/src/decode-page-route/__snapshots__/index.test.jsx.snap
deleted file mode 100644
index 9a9bd772fa..0000000000
--- a/src/decode-page-route/__snapshots__/index.test.jsx.snap
+++ /dev/null
@@ -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`] = `
-
- PageWrap: {
- "children": [
- " ",
- [
- " ",
- [],
- " "
- ],
- " "
- ]
-}
-
-`;
diff --git a/src/decode-page-route/index.test.jsx b/src/decode-page-route/index.test.jsx
index 8abf352dbd..81f22517a8 100644
--- a/src/decode-page-route/index.test.jsx
+++ b/src/decode-page-route/index.test.jsx
@@ -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', () => {
diff --git a/src/index.test.jsx b/src/index.test.jsx
index 4ff17b46ff..27e9bb819d 100644
--- a/src/index.test.jsx
+++ b/src/index.test.jsx
@@ -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();
});
});
diff --git a/src/plugin-slots/ContentIFrameLoaderSlot/README.md b/src/plugin-slots/ContentIFrameLoaderSlot/README.md
index 9ee49e5d27..be835f7831 100644
--- a/src/plugin-slots/ContentIFrameLoaderSlot/README.md
+++ b/src/plugin-slots/ContentIFrameLoaderSlot/README.md
@@ -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 }) => (
+
+
Loading course content...
+
Course: {courseId}
+
+ {defaultLoaderComponent}
+
+
Please wait while we prepare your learning experience
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/ContentIFrameLoaderSlot/images/loader-example.png b/src/plugin-slots/ContentIFrameLoaderSlot/images/loader-example.png
new file mode 100644
index 0000000000..15424c372c
Binary files /dev/null and b/src/plugin-slots/ContentIFrameLoaderSlot/images/loader-example.png differ
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/README.md b/src/plugin-slots/CourseBreadcrumbsSlot/README.md
index 12863d4e08..4538e41412 100644
--- a/src/plugin-slots/CourseBreadcrumbsSlot/README.md
+++ b/src/plugin-slots/CourseBreadcrumbsSlot/README.md
@@ -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 }) => (
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
+
### Replaced with custom component

diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx b/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
index 8a438968a1..3f672476f8 100644
--- a/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
+++ b/src/plugin-slots/CourseBreadcrumbsSlot/index.tsx
@@ -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 = ({
slotOptions={{
mergeProps: true,
}}
- >
-
-
+ pluginProps={{
+ courseId,
+ sectionId,
+ sequenceId,
+ unitId,
+ isStaff,
+ }}
+ />
);
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png
index c10ca827d5..a2e223c364 100644
Binary files a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png and b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_default.png differ
diff --git a/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png
new file mode 100644
index 0000000000..c10ca827d5
Binary files /dev/null and b/src/plugin-slots/CourseBreadcrumbsSlot/screenshot_with_default_breadcrumbs.png differ
diff --git a/src/plugin-slots/CourseOutlineTabNotificationsSlot/README.md b/src/plugin-slots/CourseOutlineTabNotificationsSlot/README.md
index 5ff67937be..fd8e60afb7 100644
--- a/src/plugin-slots/CourseOutlineTabNotificationsSlot/README.md
+++ b/src/plugin-slots/CourseOutlineTabNotificationsSlot/README.md
@@ -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 }) => (
+
+
+
📢 Course Announcement
+
+ Important updates for course {courseId}
+
+
+ Context: {model}
+
+
+ View Details
+
+
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/CourseOutlineTabNotificationsSlot/images/course-outline-notification-example.png b/src/plugin-slots/CourseOutlineTabNotificationsSlot/images/course-outline-notification-example.png
new file mode 100644
index 0000000000..3f27331f5d
Binary files /dev/null and b/src/plugin-slots/CourseOutlineTabNotificationsSlot/images/course-outline-notification-example.png differ
diff --git a/src/plugin-slots/GatedUnitContentMessageSlot/README.md b/src/plugin-slots/GatedUnitContentMessageSlot/README.md
index 2083abbe50..915010ccd0 100644
--- a/src/plugin-slots/GatedUnitContentMessageSlot/README.md
+++ b/src/plugin-slots/GatedUnitContentMessageSlot/README.md
@@ -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 }) => (
+
+
+
+
+
Premium Content
+
This content is available to verified learners only.
+
+
+
+
+ Upgrade your enrollment for course {courseId} to access:
+
+
+ ✅ Graded assignments and quizzes
+ 🏆 Verified certificate upon completion
+ 💬 Full discussion forum access
+ 📱 Mobile app offline access
+
+
+ Upgrade Now
+
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/GatedUnitContentMessageSlot/images/gated-unit-message-example.png b/src/plugin-slots/GatedUnitContentMessageSlot/images/gated-unit-message-example.png
new file mode 100644
index 0000000000..5a8200423e
Binary files /dev/null and b/src/plugin-slots/GatedUnitContentMessageSlot/images/gated-unit-message-example.png differ
diff --git a/src/plugin-slots/NotificationTraySlot/README.md b/src/plugin-slots/NotificationTraySlot/README.md
index 02bbc5f098..53fe44b091 100644
--- a/src/plugin-slots/NotificationTraySlot/README.md
+++ b/src/plugin-slots/NotificationTraySlot/README.md
@@ -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
+ }) => (
+
+
📬 Course Notifications
+
+ {notificationCurrentState === 'accessLastHour' && (
+
+
⏰ Last Chance!
+
Your access expires in less than an hour.
+
+ )}
+
+ {notificationCurrentState === 'accessDaysLeft' && (
+
+
📅 Access Reminder
+
Your course access expires in a few days.
+
+ )}
+
+
+
+ 📚 New course material available
+ setNotificationCurrentState('accessDateView')}
+ >
+ View
+
+
+
+
+
+
+ 🎯 Assignment due tomorrow
+ Due Soon
+
+
+
+
+
+ Course: {courseId} | Model: {model}
+
+
+
+ ),
+ },
+ },
+ ]
+ }
+ },
+}
+
+export default config;
+```
diff --git a/src/plugin-slots/NotificationTraySlot/images/notification-tray-slot-example.png b/src/plugin-slots/NotificationTraySlot/images/notification-tray-slot-example.png
new file mode 100644
index 0000000000..e79e21ca5f
Binary files /dev/null and b/src/plugin-slots/NotificationTraySlot/images/notification-tray-slot-example.png differ
diff --git a/src/plugin-slots/NotificationWidgetSlot/README.md b/src/plugin-slots/NotificationWidgetSlot/README.md
index 206139d5c8..3e6d5f73a3 100644
--- a/src/plugin-slots/NotificationWidgetSlot/README.md
+++ b/src/plugin-slots/NotificationWidgetSlot/README.md
@@ -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 (
+ {
+ if (e.key === 'Enter' || e.key === ' ') {
+ e.preventDefault();
+ toggleSidebar();
+ }
+ }}
+ >
+
+
+ {notification.icon}
+
+
+ {notification.title}
+ {notification.message}
+
+
+
+
+
+
+ );
+}
+
+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;
+```
diff --git a/src/plugin-slots/NotificationWidgetSlot/images/notification-widget-slot-example.png b/src/plugin-slots/NotificationWidgetSlot/images/notification-widget-slot-example.png
new file mode 100644
index 0000000000..dcd5503e79
Binary files /dev/null and b/src/plugin-slots/NotificationWidgetSlot/images/notification-widget-slot-example.png differ
diff --git a/src/plugin-slots/SequenceNavigationSlot/README.md b/src/plugin-slots/SequenceNavigationSlot/README.md
index b86816253b..ba2ffeb9d7 100644
--- a/src/plugin-slots/SequenceNavigationSlot/README.md
+++ b/src/plugin-slots/SequenceNavigationSlot/README.md
@@ -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 }) => (
+
+ ),
+ },
+ },
+ ],
+ },
+ },
+};
+
+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.
diff --git a/src/plugin-slots/SequenceNavigationSlot/index.jsx b/src/plugin-slots/SequenceNavigationSlot/index.jsx
index c48652a4c0..b4ceb2f409 100644
--- a/src/plugin-slots/SequenceNavigationSlot/index.jsx
+++ b/src/plugin-slots/SequenceNavigationSlot/index.jsx
@@ -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,
}}
- >
-
-
+ />
);
SequenceNavigationSlot.propTypes = {
diff --git a/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png b/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png
index 036cca1c3f..93b8c0cb5f 100644
Binary files a/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png and b/src/plugin-slots/SequenceNavigationSlot/screenshot_default.png differ
diff --git a/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png b/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png
new file mode 100644
index 0000000000..036cca1c3f
Binary files /dev/null and b/src/plugin-slots/SequenceNavigationSlot/screenshot_with_default_nav.png differ
diff --git a/src/plugin-slots/UnitTitleSlot/README.md b/src/plugin-slots/UnitTitleSlot/README.md
index 4da59b47b3..9308ddfddb 100644
--- a/src/plugin-slots/UnitTitleSlot/README.md
+++ b/src/plugin-slots/UnitTitleSlot/README.md
@@ -8,12 +8,13 @@
### Props:
* `unitId`
* `unit`
-* `isEnabledOutlineSidebar`
* `renderUnitNavigation`
## Description
This slot is used for adding content before or after the Unit title.
+`isEnabledOutlineSidebar` is no longer used in the default implementation,
+but is still passed as a plugin prop with a default value of `true` for backward compatibility.
## Example
@@ -34,9 +35,9 @@ const config = {
widget: {
id: 'custom_unit_title_content',
type: DIRECT_PLUGIN,
- RenderWidget: ({ unitId, unit, isEnabledOutlineSidebar, renderUnitNavigation }) => (
+ RenderWidget: ({ unitId, unit, renderUnitNavigation }) => (
<>
- {isEnabledOutlineSidebar && renderUnitNavigation(true)}
+ {renderUnitNavigation(true)}
📙: {unit.title}
📙: {unitId}
>
diff --git a/src/plugin-slots/UnitTitleSlot/index.jsx b/src/plugin-slots/UnitTitleSlot/index.jsx
index 237f35e0e5..05aa728724 100644
--- a/src/plugin-slots/UnitTitleSlot/index.jsx
+++ b/src/plugin-slots/UnitTitleSlot/index.jsx
@@ -8,7 +8,6 @@ import messages from '@src/courseware/course/sequence/messages';
const UnitTitleSlot = ({
unitId,
unit,
- isEnabledOutlineSidebar,
renderUnitNavigation,
}) => {
const { formatMessage } = useIntl();
@@ -21,7 +20,7 @@ const UnitTitleSlot = ({
pluginProps={{
unitId,
unit,
- isEnabledOutlineSidebar,
+ isEnabledOutlineSidebar: true,
renderUnitNavigation,
}}
>
@@ -29,7 +28,7 @@ const UnitTitleSlot = ({
{unit.title}
- {isEnabledOutlineSidebar && renderUnitNavigation(true)}
+ {renderUnitNavigation(true)}
{formatMessage(messages.headerPlaceholder)}
{
});
it.each([true, false])(
- 'should load courseware checkpoint correctly if tour enabled is $showCoursewareTour',
+ 'displays courseware checkpoint only when $showCoursewareTour is enabled',
async (showCoursewareTour) => {
axiosMock.onGet(tourDataUrl).reply(200, {
course_home_tour_status: 'no-tour',
@@ -293,13 +293,6 @@ describe('Courseware Tour', () => {
});
const container = 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}/${defaultSequenceBlock.id}/${unitBlocks[1].id}`);
-
const checkpoint = container.querySelectorAll('#pgn__checkpoint');
expect(checkpoint).toHaveLength(showCoursewareTour ? 1 : 0);
},
diff --git a/src/setupTest.js b/src/setupTest.js
index 0e51c8148e..60a87751e1 100755
--- a/src/setupTest.js
+++ b/src/setupTest.js
@@ -177,8 +177,6 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
courseHomeMetadataUrl = appendBrowserTimezoneToUrl(courseHomeMetadataUrl);
const provider = options?.provider || 'legacy';
- const enableNavigationSidebar = options.enableNavigationSidebar || { enable_navigation_sidebar: true };
- const alwaysOpenAuxiliarySidebar = options.alwaysOpenAuxiliarySidebar || { always_open_auxiliary_sidebar: true };
const enableCompletionTracking = options.enableCompletionTracking || { enable_completion_tracking: true };
axiosMock.onGet(courseMetadataUrl).reply(200, courseMetadata);
@@ -186,8 +184,6 @@ export async function initializeTestStore(options = {}, overrideStore = true) {
axiosMock.onGet(learningSequencesUrlRegExp).reply(200, buildOutlineFromBlocks(courseBlocks));
axiosMock.onGet(discussionConfigUrl).reply(200, { provider });
axiosMock.onGet(coursewareSidebarSettingsUrl).reply(200, {
- ...enableNavigationSidebar,
- ...alwaysOpenAuxiliarySidebar,
...enableCompletionTracking,
});