Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
d59773f5be chore(deps): bump form-data from 4.0.2 to 4.0.4
Bumps [form-data](https://github.com/form-data/form-data) from 4.0.2 to 4.0.4.
- [Release notes](https://github.com/form-data/form-data/releases)
- [Changelog](https://github.com/form-data/form-data/blob/master/CHANGELOG.md)
- [Commits](https://github.com/form-data/form-data/compare/v4.0.2...v4.0.4)

---
updated-dependencies:
- dependency-name: form-data
  dependency-version: 4.0.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-07-22 06:09:33 +00:00
51 changed files with 3896 additions and 1565 deletions

2
.env
View File

@@ -12,7 +12,6 @@ CREDIT_HELP_LINK_URL=''
CSRF_TOKEN_API_PATH=''
DISCOVERY_API_BASE_URL=''
DISCUSSIONS_MFE_BASE_URL=''
DISCOUNT_CODE_INFO_URL=''
ECOMMERCE_BASE_URL=''
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
@@ -53,4 +52,3 @@ OPTIMIZELY_FULL_STACK_SDK_KEY=''
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
# Fallback in local style files
PARAGON_THEME_URLS={}
FEATURE_ENABLE_CHAT_V2_ENDPOINT=''

View File

@@ -12,7 +12,6 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
DISCOUNT_CODE_INFO_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
@@ -55,4 +54,3 @@ OPTIMIZELY_FULL_STACK_SDK_KEY=''
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
# Fallback in local style files
PARAGON_THEME_URLS={}
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'

View File

@@ -12,7 +12,6 @@ CREDIT_HELP_LINK_URL='https://help.edx.org/edxlearner/s/article/Can-I-receive-co
CSRF_TOKEN_API_PATH='/csrf/api/v1/token'
DISCOVERY_API_BASE_URL='http://localhost:18381'
DISCUSSIONS_MFE_BASE_URL='http://localhost:2002'
DISCOUNT_CODE_INFO_URL=''
ECOMMERCE_BASE_URL='http://localhost:18130'
ENABLE_JUMPNAV='true'
ENABLE_NOTICES=''
@@ -51,4 +50,3 @@ USER_INFO_COOKIE_NAME='edx-user-info'
PRIVACY_POLICY_URL='http://localhost:18000/privacy'
SHOW_UNGRADED_ASSIGNMENT_PROGRESS=''
ENTERPRISE_LEARNER_PORTAL_URL='http://localhost:Enterprise'
FEATURE_ENABLE_CHAT_V2_ENDPOINT='false'

View File

@@ -10,8 +10,8 @@ jobs:
tests:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v6
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version-file: '.nvmrc'
- run: make validate.ci
@@ -24,11 +24,11 @@ jobs:
runs-on: ubuntu-latest
needs: tests
steps:
- uses: actions/checkout@v5
- uses: actions/checkout@v4
- name: Download code coverage results
uses: actions/download-artifact@v5
uses: actions/download-artifact@v4
with:
pattern: code-coverage-report
name: code-coverage-report
- name: Upload coverage
uses: codecov/codecov-action@v5
with:

2
.nvmrc
View File

@@ -1 +1 @@
24
20

1210
package-lock.json generated
View File

@@ -12,16 +12,17 @@
"@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": "^8.0.0",
"@edx/frontend-lib-learning-assistant": "^2.23.1",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-lib-learning-assistant": "^2.20.0",
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.4.0",
"@edx/frontend-platform": "^8.3.1",
"@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.6.2",
"@openedx/frontend-build": "^14.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@popperjs/core": "2.11.8",
@@ -51,6 +52,7 @@
"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",
@@ -68,7 +70,6 @@
"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": {
@@ -123,14 +124,14 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
"integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/helper-validator-identifier": "^7.25.9",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
"picocolors": "^1.0.0"
},
"engines": {
"node": ">=6.9.0"
@@ -150,7 +151,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.10.tgz",
"integrity": "sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.26.2",
@@ -413,9 +413,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"version": "7.25.9",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
"integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -2139,7 +2139,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2162,7 +2161,6 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2210,8 +2208,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.5.0.tgz",
"integrity": "sha512-d2ggwi5j4DOBJOwhWZxBWQSDR0DhT4ke/1PbzRauICdFkuOyax+PsFjK8GUh443K2OaQpy9PGfiCzZ1Yg37AUA==",
"license": "AGPL-3.0",
"peer": true
"license": "AGPL-3.0"
},
"node_modules/@edx/eslint-config": {
"version": "4.3.0",
@@ -2231,16 +2228,16 @@
}
},
"node_modules/@edx/frontend-component-footer": {
"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==",
"version": "14.9.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-footer/-/frontend-component-footer-14.9.0.tgz",
"integrity": "sha512-eI3ffSvAKDoP1K/vBuQrCL7ca0U7hJqRlGjBq62dSAlsgOS59t+9T1ZkIWCSoRUzHLv4fS+taiBeCsIQVV356Q==",
"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.6",
"@fortawesome/react-fontawesome": "0.2.2",
"@openedx/frontend-plugin-framework": "^1.7.0",
"classnames": "^2.5.1",
"jest-environment-jsdom": "^29.7.0",
@@ -2292,31 +2289,34 @@
}
},
"node_modules/@edx/frontend-component-footer/node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
"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==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
"@fortawesome/fontawesome-svg-core": "~1 || ~6",
"react": ">=16.3"
}
},
"node_modules/@edx/frontend-component-header": {
"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==",
"version": "6.4.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-component-header/-/frontend-component-header-6.4.0.tgz",
"integrity": "sha512-RNV3XRXhhN9QlhAoP26CjzoRIPlLSYDp3PZCnK6g6kIHgxC9dCpu2PTZdxV2AVChqVuxtZK5zLbk9yeAtf4U/A==",
"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/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/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,59 +2329,93 @@
"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.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==",
"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==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-regular-svg-icons": {
"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==",
"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==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/free-solid-svg-icons": {
"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==",
"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==",
"license": "(CC-BY-4.0 AND MIT)",
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
"@fortawesome/fontawesome-common-types": "6.6.0"
},
"engines": {
"node": ">=6"
}
},
"node_modules/@edx/frontend-component-header/node_modules/@fortawesome/react-fontawesome": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/@fortawesome/react-fontawesome/-/react-fontawesome-0.2.6.tgz",
"integrity": "sha512-mtBFIi1UsYQo7rYonYFkjgYKGoL8T+fEH6NGUpvuqtY3ytMsAoDaPo5rk25KuMtKDipY4bGYM/CkmCHA1N3FUg==",
"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==",
"license": "MIT",
"dependencies": {
"prop-types": "^15.8.1"
},
"peerDependencies": {
"@fortawesome/fontawesome-svg-core": "~1 || ~6 || ~7",
"react": "^16.3 || ^17.0.0 || ^18.0.0 || ^19.0.0"
"@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"
}
},
"node_modules/@edx/frontend-lib-learning-assistant": {
"version": "2.23.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.23.1.tgz",
"integrity": "sha512-0rDHlE3tlADWOcqKaVIKkMK2YGonbRaYJfmBSgH+Sn6+BFg2e541fn7NC9e5rIaiV1BnMREF7dxyRa/IEYLZLA==",
"version": "2.21.0",
"resolved": "https://registry.npmjs.org/@edx/frontend-lib-learning-assistant/-/frontend-lib-learning-assistant-2.21.0.tgz",
"integrity": "sha512-CUzPCQaBgXi6E1kvY0nyBSVFu8RUGpwKH4V0p8ZuysyHyRHpA+339b+gEi9FvVBMP/X4IxZHsZhi7nphlr43Iw==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.3",
@@ -2447,7 +2481,6 @@
"integrity": "sha512-0KNN0nc5eIzaJxlv43QcDmTkDY1CqeN6J7OCGSs+fwGPdtv0yOQqRjieopBCmw+yd7uD3N2HeNL3Zm5isDleLg==",
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "^0.2.34"
},
@@ -2509,7 +2542,6 @@
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.4.0.tgz",
"integrity": "sha512-toWMU7qVx56f5bLk6/Sl5WWqlKtGp602qDs22JYp5r2VBp5F/nzcrpXXWC925/kH0TP5hI2OMolmLq6n2N8a4Q==",
"license": "AGPL-3.0",
"peer": true,
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
@@ -2570,6 +2602,54 @@
"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",
@@ -2591,9 +2671,9 @@
}
},
"node_modules/@emnapi/runtime": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"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==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3081,7 +3161,6 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -3233,424 +3312,6 @@
"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",
@@ -3954,7 +3615,6 @@
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz",
"integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
"@jest/console": "^29.7.0",
@@ -4054,7 +3714,6 @@
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz",
"integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.11.6",
"@jest/types": "^29.6.3",
@@ -4081,7 +3740,6 @@
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz",
"integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/schemas": "^29.6.3",
"@types/istanbul-lib-coverage": "^2.0.0",
@@ -4358,11 +4016,10 @@
}
},
"node_modules/@openedx/frontend-build": {
"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==",
"version": "14.6.1",
"resolved": "https://registry.npmjs.org/@openedx/frontend-build/-/frontend-build-14.6.1.tgz",
"integrity": "sha512-HPswCfxThP0F92fmKqOetQ+E7HNiXDmOE+vHkfrpdKYNUj6Sn+7jaBICn8pNfif8uq4tF2ZGRnAgfUphry2ORQ==",
"license": "AGPL-3.0",
"peer": true,
"dependencies": {
"@babel/cli": "7.24.8",
"@babel/core": "7.24.9",
@@ -4407,7 +4064,7 @@
"file-loader": "6.2.0",
"html-webpack-plugin": "5.6.3",
"identity-obj-proxy": "3.0.0",
"image-minimizer-webpack-plugin": "4.1.4",
"image-minimizer-webpack-plugin": "3.8.3",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"mini-css-extract-plugin": "1.6.2",
@@ -4415,13 +4072,13 @@
"postcss": "8.4.49",
"postcss-custom-media": "10.0.8",
"postcss-loader": "7.3.4",
"postcss-rtlcss": "5.7.1",
"postcss-rtlcss": "5.1.2",
"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.34.3",
"sharp": "0.32.6",
"source-map-loader": "4.0.2",
"style-loader": "3.3.4",
"ts-jest": "29.1.4",
@@ -4447,7 +4104,6 @@
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.9.tgz",
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -4519,7 +4175,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -4581,7 +4236,6 @@
"resolved": "https://registry.npmjs.org/sass/-/sass-1.85.1.tgz",
"integrity": "sha512-Uk8WpxM5v+0cMR0XjX9KfRIacmSG86RH4DCCZjLU2rFh5tyutt9siAXJ7G+YfxQ99Q6wrRMbMlVl6KqUms71ag==",
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -4795,7 +4449,6 @@
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.4.5.tgz",
"integrity": "sha512-baBTZDO6hdCjI+Jj3oSQaz5GkFTR2TEs94pPMOls7bc/Fsv4zQIN8xDPo4NzAVo/2+3eSuEzUz3xzBeb+94rtw==",
"license": "Apache-2.0",
"peer": true,
"workspaces": [
"example",
"component-generator",
@@ -5516,7 +5169,6 @@
"resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==",
"license": "MIT",
"peer": true,
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
@@ -5527,7 +5179,6 @@
"resolved": "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-1.9.7.tgz",
"integrity": "sha512-t7v8ZPxhhKgOKtU+uyJT13lu4vL7az5aFi4IdoDs/eS548edn2M8Ik9h8fxgvMjGoAUVFSt6ZC1P5cWmQ014QQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"immer": "^9.0.21",
"redux": "^4.2.1",
@@ -5799,7 +5450,6 @@
"resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz",
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -5902,9 +5552,7 @@
"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",
@@ -5923,7 +5571,6 @@
"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",
@@ -5944,7 +5591,6 @@
"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",
@@ -5958,14 +5604,12 @@
"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"
@@ -6061,7 +5705,6 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
"license": "MIT"
},
"node_modules/@types/babel__core": {
@@ -6473,7 +6116,6 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
"integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
@@ -6610,7 +6252,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-5.62.0.tgz",
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -6657,7 +6298,6 @@
"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-5.62.0.tgz",
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -7324,7 +6964,6 @@
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==",
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -7400,7 +7039,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -7888,7 +7526,6 @@
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.6",
"form-data": "^4.0.0",
@@ -7938,12 +7575,17 @@
"node": ">= 0.4"
}
},
"node_modules/b4a": {
"version": "1.6.7",
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.7.tgz",
"integrity": "sha512-OnAYlL5b7LEkALw87fUVafQw5rVR9RjwGd4KUwNQ6DrrNmaVaUCgLipfVlzrPQ4tWOR9P0IXGNOx50jYCCdSJg==",
"license": "Apache-2.0"
},
"node_modules/babel-jest": {
"version": "29.7.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz",
"integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/transform": "^29.7.0",
"@types/babel__core": "^7.1.14",
@@ -8199,6 +7841,78 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/bare-events": {
"version": "2.5.4",
"resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.5.4.tgz",
"integrity": "sha512-+gFfDkR8pj4/TrWCGUGWmJIkBwuxPS5F+a5yWjOHQt2hHvNZd5YLzadjmDUtFmMM4y429bnKLa8bYBMHcYdnQA==",
"license": "Apache-2.0",
"optional": true
},
"node_modules/bare-fs": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.1.2.tgz",
"integrity": "sha512-8wSeOia5B7LwD4+h465y73KOdj5QHsbbuoUfPBi+pXgFJIPuG7SsiOdJuijWMyfid49eD+WivpfY7KT8gbAzBA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-events": "^2.5.4",
"bare-path": "^3.0.0",
"bare-stream": "^2.6.4"
},
"engines": {
"bare": ">=1.16.0"
},
"peerDependencies": {
"bare-buffer": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
}
}
},
"node_modules/bare-os": {
"version": "3.6.1",
"resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.6.1.tgz",
"integrity": "sha512-uaIjxokhFidJP+bmmvKSgiMzj2sV5GPHaZVAIktcxcpCyBFFWO+YlikVAdhmUo2vYFvFhOXIAlldqV29L8126g==",
"license": "Apache-2.0",
"optional": true,
"engines": {
"bare": ">=1.14.0"
}
},
"node_modules/bare-path": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz",
"integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"bare-os": "^3.0.1"
}
},
"node_modules/bare-stream": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.6.5.tgz",
"integrity": "sha512-jSmxKJNJmHySi6hC42zlZnq00rga4jjxcgNZjY9N5WlOe/iOoGRtdwGsHzQv2RlH2KOYMwGUXhf2zXd32BA9RA==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
"streamx": "^2.21.0"
},
"peerDependencies": {
"bare-buffer": "*",
"bare-events": "*"
},
"peerDependenciesMeta": {
"bare-buffer": {
"optional": true
},
"bare-events": {
"optional": true
}
}
},
"node_modules/base64-js": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
@@ -8397,7 +8111,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"caniuse-lite": "^1.0.30001688",
"electron-to-chromium": "^1.5.73",
@@ -8615,9 +8328,9 @@
}
},
"node_modules/caniuse-lite": {
"version": "1.0.30001751",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001751.tgz",
"integrity": "sha512-A0QJhug0Ly64Ii3eIqHu5X51ebln3k4yTUkY1j8drqpWHVreg/VLijN48cZ1bYPiqOQuqpkIKnzr/Ul8V+p6Cw==",
"version": "1.0.30001727",
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz",
"integrity": "sha512-pB68nIHmbN6L/4C6MH1DokyR3bYqFwjaSs/sWDHGj4CTcFtQUQMuJftVwWkXq7mNWOybD3KhUv3oWHoGxgP14Q==",
"funding": [
{
"type": "opencollective",
@@ -8858,6 +8571,12 @@
"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",
@@ -9375,6 +9094,18 @@
"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",
@@ -9605,7 +9336,6 @@
"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": {
@@ -9891,6 +9621,21 @@
"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",
@@ -9905,6 +9650,15 @@
}
}
},
"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",
@@ -10219,7 +9973,6 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
"license": "MIT"
},
"node_modules/dom-converter": {
@@ -10490,7 +10243,6 @@
"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"
@@ -10797,7 +10549,6 @@
"integrity": "sha512-0wpHoUbDUHgNCyvFB5aXLiQVfK9B0at6gUvzy83k4kAsQ/u769TQDX6iKC+aO4upIHO9WSaA3QoXYQDHbNwf1A==",
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -10854,7 +10605,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-19.0.4.tgz",
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -10895,7 +10645,6 @@
"resolved": "https://registry.npmjs.org/eslint-config-airbnb-typescript/-/eslint-config-airbnb-typescript-17.1.0.tgz",
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0"
},
@@ -11312,7 +11061,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.31.0.tgz",
"integrity": "sha512-ixmkI62Rbc2/w8Vfxyh1jQRTdRTF52VxwRVHl/ykPAmqG+Nb7/kNn+byLP0LxPgI7zWA16Jt82SybJInmMia3A==",
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.8",
@@ -11367,7 +11115,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.7.1.tgz",
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.7",
"aria-query": "^5.1.3",
@@ -11404,7 +11151,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.33.2.tgz",
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
@@ -11435,7 +11181,6 @@
"resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.1.tgz",
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -11783,6 +11528,15 @@
"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",
@@ -11905,6 +11659,12 @@
"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",
@@ -12443,14 +12203,15 @@
}
},
"node_modules/form-data": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.2.tgz",
"integrity": "sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==",
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz",
"integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==",
"license": "MIT",
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"es-set-tostringtag": "^2.1.0",
"hasown": "^2.0.2",
"mime-types": "^2.1.12"
},
"engines": {
@@ -12511,6 +12272,12 @@
"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",
@@ -12710,6 +12477,12 @@
"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",
@@ -12862,7 +12635,6 @@
"integrity": "sha512-l0xWZpoPKpppFzMfvVyFmp9vLN7w/ZZJPefUicMCepfJeQ8sMcztloGYY9DfjVPo6tIUDzU5Hw3MUbIjj9AVVA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"iterall": "^1.2.2"
},
@@ -13514,16 +13286,16 @@
}
},
"node_modules/image-minimizer-webpack-plugin": {
"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==",
"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==",
"license": "MIT",
"dependencies": {
"schema-utils": "^4.2.0",
"serialize-javascript": "^6.0.2"
"serialize-javascript": "^6.0.1"
},
"engines": {
"node": ">= 18.12.0"
"node": ">= 12.13.0"
},
"funding": {
"type": "opencollective",
@@ -13650,7 +13422,6 @@
"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"
@@ -15166,7 +14937,6 @@
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz",
"integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.6.3",
@@ -15614,9 +15384,9 @@
"license": "MIT"
},
"node_modules/js-toml": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.2.tgz",
"integrity": "sha512-/7IQ//bzn2a/5IDazPUNzlW7bsjxS51cxciYZDR+Z+3Le60yzT0YfI8KOWqTtBcZkXXVklhWd2OuGd8ZksB0wQ==",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/js-toml/-/js-toml-1.0.1.tgz",
"integrity": "sha512-rHd/IolpFm2V5BmHCEY8CckHs8NDsYZZ64H5RNgA6Opsr9vX4QyTiQPplgtqg7b3ztqYShZC38nl6CUg7QuhXg==",
"license": "MIT",
"dependencies": {
"chevrotain": "^11.0.3",
@@ -16230,7 +16000,6 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz",
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
"bin": {
"lz-string": "bin/bin.js"
@@ -16969,11 +16738,22 @@
"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"
@@ -17054,6 +16834,12 @@
"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",
@@ -17123,6 +16909,12 @@
"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",
@@ -17175,6 +16967,30 @@
"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",
@@ -18313,7 +18129,6 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.8",
"picocolors": "^1.1.1",
@@ -19009,12 +18824,12 @@
}
},
"node_modules/postcss-rtlcss": {
"version": "5.7.1",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.7.1.tgz",
"integrity": "sha512-zE68CuARv5StOG/UQLa0W1Y/raUTzgJlfjtas43yh3/G1BFmoPEaHxPRHgeowXRFFhW33FehrNgsljxRLmPVWw==",
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/postcss-rtlcss/-/postcss-rtlcss-5.1.2.tgz",
"integrity": "sha512-cmcgRoO1wL7IJyVHw0RneWI/5Oe75NLC2NLlQLsNI7hcui+yRcW4RrILfQa4FqKQRLTU4r5eF0YPi1qZpMzQpA==",
"license": "Apache-2.0",
"dependencies": {
"rtlcss": "4.3.0"
"rtlcss": "4.1.1"
},
"engines": {
"node": ">=18.0.0"
@@ -19073,6 +18888,69 @@
"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",
@@ -19111,7 +18989,6 @@
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"ansi-regex": "^5.0.1",
@@ -19126,7 +19003,6 @@
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -19139,7 +19015,6 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
"license": "MIT"
},
"node_modules/process": {
@@ -19182,7 +19057,6 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -19253,7 +19127,6 @@
"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",
@@ -19474,12 +19347,35 @@
"node": ">= 0.8"
}
},
"node_modules/rc": {
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz",
"integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==",
"license": "(BSD-2-Clause OR MIT OR Apache-2.0)",
"dependencies": {
"deep-extend": "^0.6.0",
"ini": "~1.3.0",
"minimist": "^1.2.0",
"strip-json-comments": "~2.0.1"
},
"bin": {
"rc": "cli.js"
}
},
"node_modules/rc/node_modules/strip-json-comments": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz",
"integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/react": {
"version": "18.3.1",
"resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
"integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -19633,7 +19529,6 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -19948,7 +19843,6 @@
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
"integrity": "sha512-Gx4L3uM182jEEayZfRbI/G11ZpYdNAnBs70lFVMNdHJI76XYtR+7m0MN+eAs7UHBPhWXcnFPaS+9owSCJQHNpQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.15.4",
"@types/react-redux": "^7.1.20",
@@ -19980,7 +19874,6 @@
"resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.16.0.tgz",
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -20055,7 +19948,6 @@
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.15.0.tgz",
"integrity": "sha512-NIytlzvzLwJkCQj2HLefmeakxxWHWAP+02EGqWEZy+DgfHHKQMUoBBjUQLOtFInBMhWtb3hiUy6MfFgwLjXhqg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@remix-run/router": "1.8.0"
},
@@ -20071,7 +19963,6 @@
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.15.0.tgz",
"integrity": "sha512-aR42t0fs7brintwBGAv2+mGlCtgtFQeOzK0BM1/OiqEzRejOZtpMZepvgkscpMUnKb8YO84G7s3LsHnnDNonbQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@remix-run/router": "1.8.0",
"react-router": "6.15.0"
@@ -20084,6 +19975,19 @@
"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",
@@ -20145,6 +20049,26 @@
"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",
@@ -20297,7 +20221,6 @@
"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",
@@ -20321,7 +20244,6 @@
"resolved": "https://registry.npmjs.org/redux/-/redux-4.2.1.tgz",
"integrity": "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.9.2"
}
@@ -20708,9 +20630,9 @@
}
},
"node_modules/rtlcss": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz",
"integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==",
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.1.1.tgz",
"integrity": "sha512-/oVHgBtnPNcggP2aVXQjSy6N1mMAfHg4GSag0QtZBlD5bdDgAHwr4pydqJGd+SUCu9260+Pjqbjwtvu7EMH1KQ==",
"license": "MIT",
"dependencies": {
"escalade": "^3.1.1",
@@ -20999,7 +20921,6 @@
"resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz",
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -21285,45 +21206,26 @@
"license": "MIT"
},
"node_modules/sharp": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"version": "0.32.6",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.32.6.tgz",
"integrity": "sha512-KyLTWwgcR9Oe4d9HwCwNM2l7+J0dUQwn/yf7S0EnTtb0eVS4RxO0eUSvxPtzT4F3SY+C4K6fqdv/DO27sJ/v/w==",
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
"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"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
"node": ">=14.15.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": {
@@ -21335,10 +21237,16 @@
"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.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"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"
@@ -21458,6 +21366,51 @@
"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",
@@ -21798,6 +21751,19 @@
"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",
@@ -22000,7 +21966,6 @@
"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"
@@ -22027,7 +21992,6 @@
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/glob": "^10.4.2",
@@ -22289,6 +22253,31 @@
"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",
@@ -22409,6 +22398,15 @@
"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",
@@ -22502,7 +22500,6 @@
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz",
"integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -22822,8 +22819,7 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD",
"peer": true
"license": "0BSD"
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -22846,6 +22842,18 @@
"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",
@@ -22872,7 +22880,6 @@
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz",
"integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==",
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -23642,7 +23649,6 @@
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.6.tgz",
"integrity": "sha512-TJOLrJ6oeccsGWPl7ujCYuc0pIq2cNsuD6GZDma8i5o5Npvcco/z+NKvZSFsP0/x6SShVb0+X2JK/JHUjKY9dQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6",
@@ -23745,7 +23751,6 @@
"resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz",
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -23832,7 +23837,6 @@
"resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz",
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",

View File

@@ -15,6 +15,7 @@
"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",
@@ -35,16 +36,17 @@
"@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": "^8.0.0",
"@edx/frontend-lib-learning-assistant": "^2.23.1",
"@edx/frontend-component-header": "^6.2.0",
"@edx/frontend-lib-learning-assistant": "^2.20.0",
"@edx/frontend-lib-special-exams": "^4.0.0",
"@edx/frontend-platform": "^8.4.0",
"@edx/frontend-platform": "^8.3.1",
"@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.6.2",
"@openedx/frontend-build": "^14.5.0",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.5",
"@popperjs/core": "2.11.8",
@@ -74,6 +76,7 @@
"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",

View File

@@ -0,0 +1,228 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`app registry subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element 1`] = `
<React Strict Mode>
<ErrorPage
message="test-error-message"
/>
</React Strict Mode>
`;
exports[`app registry subscribe: APP_READY. links App to root element 1`] = `
<React Strict Mode>
<AppProvider
store={
{
"dispatch": [Function],
"getState": [Function],
"replaceReducer": [Function],
"subscribe": [Function],
Symbol(Symbol.observable): [Function],
}
}
>
<HelmetWrapper
defer={true}
encodeSpecialCharacters={true}
>
<link
href="favicon-url"
rel="shortcut icon"
type="image/x-icon"
/>
</HelmetWrapper>
<PathFixesProvider>
<NoticesProvider>
<UserMessagesProvider>
<div
className="app-container"
>
<Routes>
<Route
element={
<PageWrap>
<Page Not Found />
</PageWrap>
}
path="*"
/>
<Route
element={
<PageWrap>
<Goal Unsubscribe />
</PageWrap>
}
path="/goal-unsubscribe/:token"
/>
<Route
element={
<PageWrap>
<Courseware Redirect Landing Page />
</PageWrap>
}
path="/redirect/*"
/>
<Route
element={
<PageWrap>
<Preferences Unsubscribe />
</PageWrap>
}
path="/preferences-unsubscribe/:userToken/:updatePatch"
/>
<Route
element={
<DecodePageRoute>
<Course Access Error Page />
</DecodePageRoute>
}
path="/course/:courseId/access-denied"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="outline"
>
<Outline Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/home"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="lti_live"
>
<Live Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/live"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="dates"
>
<Dates Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/dates"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseHome"
tab="discussion"
>
<Discussion Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/discussion/:path/*"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress/:targetUserId/"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
isProgressTab={true}
slice="courseHome"
tab="progress"
>
<Progress Tab />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/progress"
/>
<Route
element={
<DecodePageRoute>
<Tab Container
fetch={[Function]}
slice="courseware"
tab="courseware"
>
<Course Exit />
</Tab Container>
</DecodePageRoute>
}
path="/course/:courseId/course-end"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId/:sequenceId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/course/:courseId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId/:unitId"
/>
<Route
element={
<DecodePageRoute>
<Courseware Container />
</DecodePageRoute>
}
path="/preview/course/:courseId/:sequenceId"
/>
</Routes>
</div>
</UserMessagesProvider>
</NoticesProvider>
</PathFixesProvider>
</AppProvider>
</React Strict Mode>
`;

View File

@@ -22,7 +22,7 @@ export const DECODE_ROUTES = {
export const ROUTES = {
UNSUBSCRIBE: '/goal-unsubscribe/:token',
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch?',
PREFERENCES_UNSUBSCRIBE: '/preferences-unsubscribe/:userToken/:updatePatch',
REDIRECT: '/redirect/*',
DASHBOARD: 'dashboard',
ENTERPRISE_LEARNER_DASHBOARD: 'enterprise-learner-dashboard',

View File

@@ -5,7 +5,6 @@ import {
screen,
} from '../../setupTest';
import CoursewareSearchEmpty from './CoursewareSearchEmpty';
import messages from './messages';
function renderComponent() {
const { container } = render(<CoursewareSearchEmpty />);
@@ -17,12 +16,9 @@ describe('CoursewareSearchEmpty', () => {
initializeMockApp();
});
it('render empty results text and corresponding classes', () => {
it('should match the snapshot', () => {
renderComponent();
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');
expect(screen.getByTestId('no-results')).toMatchSnapshot();
});
});

View File

@@ -7,7 +7,6 @@ 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');
@@ -35,53 +34,8 @@ describe('CoursewareSearchResults', () => {
renderComponent({ results });
});
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');
it('should match the snapshot', () => {
expect(screen.getByTestId('search-results')).toMatchSnapshot();
});
});
});

View File

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

View File

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

View File

@@ -0,0 +1,306 @@
// 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,
}
`;

View File

@@ -10,8 +10,8 @@ describe('mapSearchResponse', () => {
response = mapSearchResponse(camelCaseObject(mockedResponse));
});
it('should match number of results', () => {
expect(response.results.length).toBe(mockedResponse.results.length);
it('should match snapshot', () => {
expect(response).toMatchSnapshot();
});
it('should match expected filters', () => {
@@ -24,25 +24,6 @@ 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', () => {

View File

@@ -0,0 +1,941 @@
// 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",
"proctoringPanelStatus": "loading",
"showSearch": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
},
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
"hasAccess": true,
"userFragment": null,
"userMessage": null,
},
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
},
],
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
"price": 149,
"sku": "8CF08E5",
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
},
},
},
"dates": {
"course-v1:edX+DemoX+Demo_Course": {
"courseDateBlocks": [
{
"date": "2020-05-01T17:59:41Z",
"dateType": "course-start-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "",
"title": "Course Starts",
},
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-04T02:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"title": "Multi Badges Completed",
},
{
"assignmentType": "Homework",
"date": "2020-05-05T02:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"title": "Multi Badges Past Due",
},
{
"assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "Both Past Due 1",
},
{
"assignmentType": "Homework",
"date": "2020-05-27T02:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "Both Past Due 2",
},
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-28T08:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "One Completed/Due 1",
},
{
"assignmentType": "Homework",
"date": "2020-05-28T08:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "One Completed/Due 2",
},
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-29T08:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "Both Completed 1",
},
{
"assignmentType": "Homework",
"complete": true,
"date": "2020-05-29T08:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "Both Completed 2",
},
{
"date": "2020-06-16T17:59:40.942669Z",
"dateType": "verified-upgrade-deadline",
"description": "Don't miss the opportunity to highlight your new knowledge and skills by earning a verified certificate.",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "Upgrade to Verified Certificate",
},
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": false,
"link": "https://example.com/",
"title": "One Verified 1",
},
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "One Verified 2",
},
{
"assignmentType": "Homework",
"date": "2030-08-17T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": "ORA Dates are set by the instructor, and can't be changed",
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "ORA Verified 2",
},
{
"assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": false,
"link": "https://example.com/",
"title": "Both Verified 1",
},
{
"assignmentType": "Homework",
"date": "2030-08-18T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": false,
"link": "https://example.com/",
"title": "Both Verified 2",
},
{
"assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"learnerHasAccess": true,
"title": "One Unreleased 1",
},
{
"assignmentType": "Homework",
"date": "2030-08-19T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "https://example.com/",
"title": "One Unreleased 2",
},
{
"assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"title": "Both Unreleased 1",
},
{
"assignmentType": "Homework",
"date": "2030-08-20T05:59:40.942669Z",
"dateType": "assignment-due-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"title": "Both Unreleased 2",
},
{
"date": "2030-08-23T00:00:00Z",
"dateType": "course-end-date",
"description": "",
"extraInfo": null,
"learnerHasAccess": true,
"link": "",
"title": "Course Ends",
},
{
"date": "2030-09-01T00:00:00Z",
"dateType": "verification-deadline-date",
"description": "You must successfully complete verification before this date to qualify for a Verified Certificate.",
"extraInfo": null,
"learnerHasAccess": false,
"link": "https://example.com/",
"title": "Verification Deadline",
},
],
"datesBannerInfo": {
"contentTypeGatingEnabled": false,
"missedDeadlines": false,
"missedGatedContent": false,
"verifiedUpgradeLink": "http://localhost:18130/basket/add/?sku=8CF08E5",
},
"hasEnded": false,
"id": "course-v1:edX+DemoX+Demo_Course",
"learnerIsFullAccess": true,
},
},
},
"plugins": {},
"recommendations": {
"recommendationsStatus": "loading",
},
"specialExams": {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false,
"showNewUserCourseHomeTour": false,
"toursEnabled": false,
},
}
`;
exports[`Data layer integration tests Test fetchOutlineTab Should fetch, normalize, and save metadata 1`] = `
{
"courseHome": {
"courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded",
"proctoringPanelStatus": "loading",
"showSearch": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
},
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
"hasAccess": true,
"userFragment": null,
"userMessage": null,
},
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
},
],
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
"price": 149,
"sku": "8CF08E5",
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
},
},
},
"outline": {
"course-v1:edX+DemoX+Demo_Course": {
"accessExpiration": null,
"certData": {
"certStatus": null,
"certWebViewUrl": null,
"certificateAvailableDate": null,
},
"courseBlocks": {
"courses": {
"block-v1:edX+DemoX+Demo_Course+type@course+block@bcdabcdabcdabcdabcdabcdabcdabcd3": {
"hasScheduledContent": false,
"id": "course-v1:edX+DemoX+Demo_Course",
"sectionIds": [
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
],
"title": "bcdabcdabcdabcdabcdabcdabcdabcd3",
},
},
"sections": {
"block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2": {
"complete": false,
"courseId": "course-v1:edX+DemoX+Demo_Course",
"hideFromTOC": undefined,
"id": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"resumeBlock": false,
"sequenceIds": [
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
],
"title": "Title of Section",
},
},
"sequences": {
"block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1": {
"complete": false,
"description": null,
"due": null,
"effortActivities": 2,
"effortTime": 15,
"hideFromTOC": undefined,
"icon": null,
"id": "block-v1:edX+DemoX+Demo_Course+type@sequential+block@bcdabcdabcdabcdabcdabcdabcdabcd1",
"navigationDisabled": undefined,
"sectionId": "block-v1:edX+DemoX+Demo_Course+type@chapter+block@bcdabcdabcdabcdabcdabcdabcdabcd2",
"showLink": true,
"title": "Title of Sequence",
},
},
},
"courseGoals": {
"daysPerWeek": null,
"goalOptions": [],
"selectedGoal": null,
"subscribedToReminders": null,
"weeklyLearningGoalEnabled": false,
},
"courseTools": [
{
"analyticsId": "edx.bookmarks",
"title": "Bookmarks",
"url": "https://example.com/bookmarks",
},
],
"datesBannerInfo": {
"contentTypeGatingEnabled": false,
"missedDeadlines": false,
"missedGatedContent": false,
},
"datesWidget": {
"courseDateBlocks": [],
},
"enableProctoredExams": undefined,
"enrollAlert": {
"canEnroll": true,
"extraText": "Contact the administrator.",
},
"enrollmentMode": undefined,
"handoutsHtml": "<ul><li>Handout 1</li></ul>",
"hasEnded": undefined,
"hasScheduledContent": null,
"id": "course-v1:edX+DemoX+Demo_Course",
"offer": null,
"resumeCourse": {
"hasVisitedCourse": false,
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/jump_to/block-v1:edX+Test+Block@12345abcde",
},
"timeOffsetMillis": 0,
"userHasPassingGrade": undefined,
"verifiedMode": {
"accessExpirationDate": "2050-01-01T12:00:00",
"currency": "USD",
"currencySymbol": "$",
"price": 149,
"sku": "ABCD1234",
"upgradeUrl": "http://localhost:18000/dashboard",
},
"welcomeMessageHtml": "<p>Welcome to this course!</p>",
},
},
},
"plugins": {},
"recommendations": {
"recommendationsStatus": "loading",
},
"specialExams": {
"activeAttempt": null,
"allowProctoringOptOut": false,
"apiErrorMsg": "",
"exam": {
"attempt": {
"attempt_code": "",
"attempt_id": null,
"attempt_status": "",
"course_id": "",
"desktop_application_js_url": "",
"exam_display_name": "",
"exam_started_poll_url": "",
"exam_type": "",
"exam_url_path": "",
"external_id": "",
"in_timed_exam": true,
"ping_interval": null,
"taking_as_proctored": true,
"time_remaining_seconds": null,
"use_legacy_attempt_api": true,
},
"backend": "",
"content_id": "",
"course_id": "",
"due_date": null,
"exam_name": "",
"external_id": "",
"hide_after_due": false,
"id": null,
"is_active": true,
"is_practice_exam": false,
"is_proctored": false,
"prerequisite_status": {
"are_prerequisites_satisifed": true,
"declined_prerequisites": [],
"failed_prerequisites": [],
"pending_prerequisites": [],
"satisfied_prerequisites": [],
},
"time_limit_mins": null,
"type": "",
},
"examAccessToken": {
"exam_access_token": "",
"exam_access_token_expiration": "",
},
"isLoading": true,
"proctoringSettings": {
"exam_proctoring_backend": {
"download_url": "",
"instructions": [],
"name": "",
"rules": {},
},
"integration_specific_email": "",
"learner_notification_from_email": "",
"provider_name": "",
"provider_tech_support_email": "",
"provider_tech_support_phone": "",
"provider_tech_support_url": "",
},
"timeIsOver": false,
},
"tours": {
"showCoursewareTour": false,
"showExistingUserCourseHomeTour": false,
"showNewUserCourseHomeModal": false,
"showNewUserCourseHomeTour": false,
"toursEnabled": false,
},
}
`;
exports[`Data layer integration tests Test fetchProgressTab Should fetch, normalize, and save metadata 1`] = `
{
"courseHome": {
"courseId": "course-v1:edX+DemoX+Demo_Course",
"courseStatus": "loaded",
"proctoringPanelStatus": "loading",
"showSearch": false,
"targetUserId": undefined,
"toastBodyLink": null,
"toastBodyText": null,
"toastHeader": "",
},
"courseware": {
"courseId": null,
"courseOutline": {},
"courseOutlineShouldUpdate": false,
"courseOutlineStatus": "loading",
"courseStatus": "loading",
"coursewareOutlineSidebarSettings": {},
"sequenceId": null,
"sequenceMightBeUnit": false,
"sequenceStatus": "loading",
},
"learningAssistant": ObjectContaining {
"conversationId": Any<String>,
},
"models": {
"courseHomeMeta": {
"course-v1:edX+DemoX+Demo_Course": {
"canViewCertificate": true,
"celebrations": null,
"courseAccess": {
"additionalContextUserMessage": null,
"developerMessage": null,
"errorCode": null,
"hasAccess": true,
"userFragment": null,
"userMessage": null,
},
"id": "course-v1:edX+DemoX+Demo_Course",
"isEnrolled": false,
"isMasquerading": false,
"isNewDiscussionSidebarViewEnabled": false,
"isSelfPaced": false,
"isStaff": false,
"number": "DemoX",
"org": "edX",
"originalUserIsStaff": false,
"start": "2013-02-05T05:00:00Z",
"tabs": [
{
"slug": "outline",
"title": "Course",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course/",
},
{
"slug": "discussion",
"title": "Discussion",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/discussion/forum/",
},
{
"slug": "wiki",
"title": "Wiki",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/course_wiki",
},
{
"slug": "progress",
"title": "Progress",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/progress",
},
{
"slug": "instructor",
"title": "Instructor",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/instructor",
},
{
"slug": "dates",
"title": "Dates",
"url": "http://localhost:18000/courses/course-v1:edX+DemoX+Demo_Course/dates",
},
],
"title": "Demonstration Course",
"userTimezone": "UTC",
"username": "MockUser",
"verifiedMode": {
"accessExpirationDate": null,
"currency": "USD",
"currencySymbol": "$",
"price": 149,
"sku": "8CF08E5",
"upgradeUrl": "http://localhost:18130/basket/add/?sku=8CF08E5",
},
},
},
"progress": {
"course-v1:edX+DemoX+Demo_Course": {
"accessExpiration": null,
"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,
},
}
`;

View File

@@ -90,14 +90,14 @@ describe('Data layer integration tests', () => {
const state = store.getState();
expect(state.courseHome.courseStatus).toEqual('loaded');
expect(state).toEqual(expect.objectContaining({
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This UUID is generated on each run.
// Instead, we use an asymmetric matcher here.
// 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.
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).toEqual(expect.objectContaining({
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This UUID is generated on each run.
// Instead, we use an asymmetric matcher here.
// 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.
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).toEqual(expect.objectContaining({
expect(state).toMatchSnapshot({
// The Xpert chatbot (frontend-lib-learning-assistant) generates a unique UUID
// to keep track of conversations. This UUID is generated on each run.
// Instead, we use an asymmetric matcher here.
// 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.
learningAssistant: expect.objectContaining({
conversationId: expect.any(String),
}),
}));
});
});
it('Should handle the url including a targetUserId', async () => {

View File

@@ -3,7 +3,6 @@ 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';
@@ -208,7 +207,7 @@ const ProctoringInfoPanel = () => {
{isSubmissionRequired(readableStatus) && (
onboardingExamButton
)}
<Button variant="outline-primary" block href={getExternalLinkUrl('https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams')}>
<Button variant="outline-primary" block href="https://support.edx.org/hc/en-us/sections/115004169247-Taking-Timed-and-Proctored-Exams">
{intl.formatMessage(messages.proctoringReviewRequirementsButton)}
</Button>
</div>

View File

@@ -187,8 +187,7 @@ const CertificateStatus = () => {
// regardless of passing or nonpassing status
if (!canViewCertificate) {
certCase = 'notAvailable';
// use the certificate_available_date if it is available, otherwise use the end date of the course
endDate = intl.formatDate((certificateAvailableDate || end), {
endDate = intl.formatDate(end, {
year: 'numeric',
month: 'long',
day: 'numeric',

View File

@@ -17,7 +17,6 @@ const CELEBRATION_STATUSES = [
'audit_passing',
'downloadable',
'earned_but_not_available',
'not_earned_but_available_date',
'honor_passing',
'requesting',
'unverified',

View File

@@ -1,6 +1,8 @@
import PropTypes from 'prop-types';
import React from 'react';
import { ErrorPage } from '@edx/frontend-platform/react';
import { StrictDict } from '@edx/react-unit-test-utils';
import { ModalDialog } from '@openedx/paragon';
import { ContentIFrameLoaderSlot } from '../../../../plugin-slots/ContentIFrameLoaderSlot';
@@ -20,10 +22,10 @@ export const IFRAME_FEATURE_POLICY = (
'microphone *; camera *; midi *; geolocation *; encrypted-media *; clipboard-write *; autoplay *'
);
export const testIDs = {
export const testIDs = StrictDict({
contentIFrame: 'content-iframe-test-id',
modalIFrame: 'modal-iframe-test-id',
};
});
const ContentIFrame = ({
iframeUrl,

View File

@@ -1,11 +1,25 @@
import { render, screen } from '@testing-library/react';
import React from 'react';
import { ErrorPage } from '@edx/frontend-platform/react';
import { ModalDialog } from '@openedx/paragon';
import { shallow } from '@edx/react-unit-test-utils';
import PageLoading from '@src/generic/PageLoading';
import { ContentIFrameLoaderSlot } from '@src/plugin-slots/ContentIFrameLoaderSlot';
import * as hooks from './hooks';
import ContentIFrame, { IFRAME_FEATURE_POLICY } from './ContentIFrame';
import ContentIFrame, { IFRAME_FEATURE_POLICY, testIDs } from './ContentIFrame';
jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: () => <div>ErrorPage</div> }));
jest.mock('@edx/frontend-platform/react', () => ({ ErrorPage: 'ErrorPage' }));
jest.mock('@src/generic/PageLoading', () => jest.fn(() => <div>PageLoading</div>));
jest.mock('@openedx/paragon', () => jest.requireActual('@edx/react-unit-test-utils')
.mockComponents({
ModalDialog: {
Body: 'ModalDialog.Body',
},
}));
jest.mock('@src/generic/PageLoading', () => 'PageLoading');
jest.mock('./hooks', () => ({
useIFrameBehavior: jest.fn(),
@@ -53,13 +67,14 @@ const props = {
title: 'test-title',
};
let el;
describe('ContentIFrame Component', () => {
beforeEach(() => {
jest.clearAllMocks();
});
describe('behavior', () => {
beforeEach(() => {
render(<ContentIFrame {...props} />);
el = shallow(<ContentIFrame {...props} />);
});
it('initializes iframe behavior hook', () => {
expect(hooks.useIFrameBehavior).toHaveBeenCalledWith({
@@ -74,61 +89,61 @@ describe('ContentIFrame Component', () => {
});
});
describe('output', () => {
let component;
describe('if shouldShowContent', () => {
describe('if not hasLoaded', () => {
it('displays errorPage if showError', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, showError: true });
render(<ContentIFrame {...props} />);
const errorPage = screen.getByText('ErrorPage');
expect(errorPage).toBeInTheDocument();
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(ErrorPage).length).toEqual(1);
});
it('displays PageLoading component if not showError', () => {
render(<ContentIFrame {...props} />);
const pageLoading = screen.getByText('PageLoading');
expect(pageLoading).toBeInTheDocument();
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ContentIFrameLoaderSlot);
expect(component.props.loadingMessage).toEqual(props.loadingMessage);
});
});
describe('hasLoaded', () => {
it('does not display PageLoading or ErrorPage', () => {
hooks.useIFrameBehavior.mockReturnValueOnce({ ...iframeBehavior, hasLoaded: true });
render(<ContentIFrame {...props} />);
const pageLoading = screen.queryByText('PageLoading');
expect(pageLoading).toBeNull();
const errorPage = screen.queryByText('ErrorPage');
expect(errorPage).toBeNull();
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(PageLoading).length).toEqual(0);
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
});
});
it('display iframe with props from hooks', () => {
render(<ContentIFrame {...props} />);
const iframe = screen.getByTitle(props.title);
expect(iframe).toBeInTheDocument();
expect(iframe).toHaveAttribute('id', props.elementId);
expect(iframe).toHaveAttribute('src', props.iframeUrl);
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('allowfullscreen', '');
expect(iframe).toHaveAttribute('scrolling', 'no');
expect(iframe).toHaveAttribute('referrerpolicy', 'origin');
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByTestId(testIDs.contentIFrame);
expect(component.props).toEqual({
allow: IFRAME_FEATURE_POLICY,
allowFullScreen: true,
scrolling: 'no',
referrerPolicy: 'origin',
title: props.title,
id: props.elementId,
src: props.iframeUrl,
height: iframeBehavior.iframeHeight,
onLoad: iframeBehavior.handleIFrameLoad,
'data-testid': testIDs.contentIFrame,
});
});
});
describe('if not shouldShowContent', () => {
it('does not show PageLoading, ErrorPage, or unit-iframe-wrapper', () => {
render(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
expect(screen.queryByText('PageLoading')).toBeNull();
expect(screen.queryByText('ErrorPage')).toBeNull();
expect(screen.queryByTitle(props.title)).toBeNull();
el = shallow(<ContentIFrame {...{ ...props, shouldShowContent: false }} />);
expect(el.instance.findByType(PageLoading).length).toEqual(0);
expect(el.instance.findByType(ErrorPage).length).toEqual(0);
expect(el.instance.findByTestId(testIDs.contentIFrame).length).toEqual(0);
});
});
it('does not display modal if modalOptions returns isOpen: false', () => {
render(<ContentIFrame {...props} />);
const modal = screen.queryByRole('dialog');
expect(modal).toBeNull();
el = shallow(<ContentIFrame {...props} />);
expect(el.instance.findByType(ModalDialog).length).toEqual(0);
});
describe('if modalOptions.isOpen', () => {
const testModalOpenAndHandleClose = () => {
it('closes modal on close button click', () => {
const closeButton = screen.getByTestId('modal-backdrop');
closeButton.click();
expect(modalIFrameData.handleModalClose).toHaveBeenCalled();
test('Modal component isOpen, with handleModalClose from hook', () => {
expect(component.props.onClose).toEqual(modalIFrameData.handleModalClose);
});
};
describe('fullscreen modal', () => {
@@ -138,13 +153,14 @@ describe('ContentIFrame Component', () => {
...modalIFrameData,
modalOptions: { ...modalOptions.withBody, isFullscreen: true },
});
render(<ContentIFrame {...props} />);
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ModalDialog);
});
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
const dialog = screen.getByRole('dialog');
expect(dialog).toBeInTheDocument();
const modalBody = screen.getByText(modalOptions.withBody.body);
expect(modalBody).toBeInTheDocument();
const content = component.findByType(ModalDialog.Body)[0].children[0];
expect(content.matches(shallow(
<div className="unit-modal">{modalOptions.withBody.body}</div>,
))).toEqual(true);
});
testModalOpenAndHandleClose();
});
@@ -155,42 +171,55 @@ describe('ContentIFrame Component', () => {
...modalIFrameData,
modalOptions: { ...modalOptions.withUrl, isFullscreen: true },
});
render(<ContentIFrame {...props} />);
});
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
const iframe = screen.getByTitle(modalOptions.withUrl.title);
expect(iframe).toBeInTheDocument();
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ModalDialog);
});
testModalOpenAndHandleClose();
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
const content = component.findByType(ModalDialog.Body)[0].children[0];
expect(content.matches(shallow(
<iframe
title={modalOptions.withUrl.title}
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.withUrl.url}
style={{ width: '100%', height: modalOptions.withUrl.height }}
/>,
))).toEqual(true);
});
});
});
describe('body modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withBody });
render(<ContentIFrame {...props} />);
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ModalDialog);
});
it('displays Modal with div wrapping provided body content if modal.body is provided', () => {
const dialog = screen.getByRole('dialog');
expect(dialog).toBeInTheDocument();
const modalBody = screen.getByText(modalOptions.withBody.body);
expect(modalBody).toBeInTheDocument();
const content = component.findByType(ModalDialog.Body)[0].children[0];
expect(content.matches(shallow(<div className="unit-modal">{modalOptions.withBody.body}</div>))).toEqual(true);
});
testModalOpenAndHandleClose();
});
describe('url modal', () => {
beforeEach(() => {
hooks.useModalIFrameData.mockReturnValueOnce({ ...modalIFrameData, modalOptions: modalOptions.withUrl });
render(<ContentIFrame {...props} />);
});
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
const iframe = screen.getByTitle(modalOptions.withUrl.title);
expect(iframe).toBeInTheDocument();
expect(iframe).toHaveAttribute('allow', IFRAME_FEATURE_POLICY);
expect(iframe).toHaveAttribute('src', modalOptions.withUrl.url);
el = shallow(<ContentIFrame {...props} />);
[component] = el.instance.findByType(ModalDialog);
});
testModalOpenAndHandleClose();
it('displays Modal with iframe to provided url if modal.body is not provided', () => {
const content = component.findByType(ModalDialog.Body)[0].children[0];
expect(content.matches(shallow(
<iframe
title={modalOptions.withUrl.title}
allow={IFRAME_FEATURE_POLICY}
frameBorder="0"
src={modalOptions.withUrl.url}
style={{ width: '100%', height: modalOptions.withUrl.height }}
/>,
))).toEqual(true);
});
});
});
});

View File

@@ -1,15 +1,22 @@
import { render, screen } from '@testing-library/react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import React from 'react';
import { formatMessage, shallow } from '@edx/react-unit-test-utils';
import { useModel } from '@src/generic/model-store';
import PageLoading from '@src/generic/PageLoading';
import { GatedUnitContentMessageSlot } from '@src/plugin-slots/GatedUnitContentMessageSlot';
import messages from '../messages';
import HonorCode from '../honor-code';
import LockPaywall from '../lock-paywall';
import hooks from './hooks';
import { modelKeys } from './constants';
import UnitSuspense from './UnitSuspense';
jest.mock('@edx/frontend-platform/i18n', () => ({
...jest.requireActual('@edx/frontend-platform/i18n'),
defineMessages: m => m,
useIntl: () => ({ formatMessage: jest.requireActual('@edx/react-unit-test-utils').formatMessage }),
}));
jest.mock('react', () => ({
@@ -17,9 +24,10 @@ jest.mock('react', () => ({
Suspense: 'Suspense',
}));
jest.mock('../honor-code', () => jest.fn(() => <div>HonorCode</div>));
jest.mock('../lock-paywall', () => jest.fn(() => <div>LockPaywall</div>));
jest.mock('../honor-code', () => 'HonorCode');
jest.mock('../lock-paywall', () => 'LockPaywall');
jest.mock('@src/generic/model-store', () => ({ useModel: jest.fn() }));
jest.mock('@src/generic/PageLoading', () => 'PageLoading');
jest.mock('./hooks', () => ({
useShouldDisplayHonorCode: jest.fn(() => false),
@@ -38,6 +46,7 @@ const props = {
id: 'test-id',
};
let el;
describe('UnitSuspense component', () => {
beforeEach(() => {
jest.clearAllMocks();
@@ -45,7 +54,7 @@ describe('UnitSuspense component', () => {
});
describe('behavior', () => {
it('initializes models', () => {
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
el = shallow(<UnitSuspense {...props} />);
const { calls } = useModel.mock;
const [unitCall] = calls.filter(call => call[0] === modelKeys.units);
const [metaCall] = calls.filter(call => call[0] === modelKeys.coursewareMeta);
@@ -57,9 +66,8 @@ describe('UnitSuspense component', () => {
describe('LockPaywall', () => {
const testNoPaywall = () => {
it('does not display LockPaywall', () => {
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
const lockPaywall = screen.queryByText('LockPaywall');
expect(lockPaywall).toBeNull();
el = shallow(<UnitSuspense {...props} />);
expect(el.instance.findByType(LockPaywall).length).toEqual(0);
});
};
describe('gating not enabled', () => { testNoPaywall(); });
@@ -70,29 +78,29 @@ describe('UnitSuspense component', () => {
describe('gating enabled, gated content included', () => {
beforeEach(() => { mockModels(true, true); });
it('displays LockPaywall in Suspense wrapper with PageLoading fallback', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
const lockPaywall = screen.getByText('LockPaywall');
expect(lockPaywall).toBeInTheDocument();
const suspenseWrapper = lockPaywall.closest('suspense');
expect(suspenseWrapper).toBeInTheDocument();
el = shallow(<UnitSuspense {...props} />);
const [component] = el.instance.findByType(GatedUnitContentMessageSlot);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingLockedContent)} />);
expect(component.props.courseId).toEqual(props.courseId);
});
});
});
describe('HonorCode', () => {
it('does not display HonorCode if useShouldDisplayHonorCode => false', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(false);
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
const honorCode = screen.queryByText('HonorCode');
expect(honorCode).toBeNull();
el = shallow(<UnitSuspense {...props} />);
expect(el.instance.findByType(HonorCode).length).toEqual(0);
});
it('displays HonorCode component in Suspense wrapper with PageLoading fallback if shouldDisplayHonorCode', () => {
hooks.useShouldDisplayHonorCode.mockReturnValueOnce(true);
render(<IntlProvider locale="en"><UnitSuspense {...props} /></IntlProvider>);
const honorCode = screen.getByText('HonorCode');
expect(honorCode).toBeInTheDocument();
const suspenseWrapper = honorCode.closest('suspense');
expect(suspenseWrapper).toBeInTheDocument();
el = shallow(<UnitSuspense {...props} />);
const [component] = el.instance.findByType(HonorCode);
expect(component.parent.type).toEqual('Suspense');
expect(component.parent.props.fallback)
.toEqual(<PageLoading srMessage={formatMessage(messages.loadingHonorCode)} />);
expect(component.props.courseId).toEqual(props.courseId);
});
});
});

View File

@@ -1,25 +1,27 @@
export const modelKeys = {
import { StrictDict } from '@edx/react-unit-test-utils/dist';
export const modelKeys = StrictDict({
units: 'units',
coursewareMeta: 'coursewareMeta',
} as const;
});
export const views = {
export const views = StrictDict({
student: 'student_view',
public: 'public_view',
} as const;
});
export const loadingState = 'loading';
export const messageTypes = {
export const messageTypes = StrictDict({
modal: 'plugin.modal',
resize: 'plugin.resize',
videoFullScreen: 'plugin.videoFullScreen',
autoAdvance: 'plugin.autoAdvance',
} as const;
});
export default {
export default StrictDict({
modelKeys,
views,
loadingState,
messageTypes,
};
});

View File

@@ -1,13 +1,19 @@
import React from 'react';
import { logError } from '@edx/frontend-platform/logging';
import { StrictDict, useKeyedState } from '@edx/react-unit-test-utils';
import { useExamAccessToken, useFetchExamAccessToken, useIsExam } from '@edx/frontend-lib-special-exams';
export const stateKeys = StrictDict({
accessToken: 'accessToken',
blockAccess: 'blockAccess',
});
const useExamAccess = ({
id,
}) => {
const isExam = useIsExam();
const [blockAccess, setBlockAccess] = React.useState(isExam);
const [blockAccess, setBlockAccess] = useKeyedState(stateKeys.blockAccess, isExam);
const fetchExamAccessToken = useFetchExamAccessToken();

View File

@@ -1,10 +1,11 @@
import React, { useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import React from 'react';
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';
@@ -17,12 +18,13 @@ import { messageTypes } from '../constants';
import useLoadBearingHook from './useLoadBearingHook';
export const iframeBehaviorState = {
iframeHeight: (val) => useState<number>(val), // eslint-disable-line
hasLoaded: (val) => useState<boolean>(val), // eslint-disable-line
showError: (val) => useState<boolean>(val), // eslint-disable-line
windowTopOffset: (val) => useState<number | null>(val), // eslint-disable-line
} as const;
export const stateKeys = StrictDict({
iframeHeight: 'iframeHeight',
hasLoaded: 'hasLoaded',
showError: 'showError',
windowTopOffset: 'windowTopOffset',
sequences: 'sequences',
});
const useIFrameBehavior = ({
elementId,
@@ -36,27 +38,27 @@ const useIFrameBehavior = ({
const dispatch = useDispatch();
const activeSequenceId = useSelector(getSequenceId);
const navigate = useNavigate();
const activeSequence = useModel('sequences', activeSequenceId);
const activeSequence = useModel(stateKeys.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);
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);
React.useEffect(() => {
const frame = document.getElementById(elementId) as HTMLIFrameElement | null;
const frame = document.getElementById(elementId);
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 }: MessageEvent) => {
const receiveMessage = React.useCallback(({ data }) => {
const { type, payload } = data;
if (type === messageTypes.resize) {
setIframeHeight(payload.height);
@@ -80,11 +82,11 @@ 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) {
if (!isLastUnit) {
navigate(nextLink);
}
}
@@ -102,36 +104,37 @@ 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) as HTMLIFrameElement | null;
const iframeElement = document.getElementById(elementId);
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);
@@ -166,9 +169,6 @@ const useIFrameBehavior = ({
dispatch(processEvent(e.data, fetchCourse));
}
};
// Update the visibility of the iframe in case the element is already visible.
updateIframeVisibility();
};
React.useEffect(() => {

View File

@@ -1,6 +1,7 @@
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';
@@ -12,7 +13,7 @@ import { useSequenceNavigationMetadata } from '@src/courseware/course/sequence/s
import { messageTypes } from '../constants';
import useIFrameBehavior, { iframeBehaviorState } from './useIFrameBehavior';
import useIFrameBehavior, { stateKeys } from './useIFrameBehavior';
const mockNavigate = jest.fn();
@@ -24,6 +25,7 @@ jest.mock('@edx/frontend-platform/analytics');
jest.mock('react', () => ({
...jest.requireActual('react'),
useEffect: jest.fn(),
useCallback: jest.fn((cb, prereqs) => ({ cb, prereqs })),
}));
@@ -32,6 +34,13 @@ jest.mock('react-redux', () => ({
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(),
}));
@@ -56,6 +65,8 @@ jest.mock('react-router-dom', () => ({
jest.mock('@src/courseware/course/sequence/sequence-navigation/hooks');
useSequenceNavigationMetadata.mockReturnValue({ isLastUnit: false, nextLink: '/next-unit-link' });
const state = mockUseKeyedState(stateKeys);
const props = {
elementId: 'test-element-id',
id: 'test-id',
@@ -93,147 +104,148 @@ 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', () => {
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);
hook = useIFrameBehavior(props);
state.expectInitializedWith(stateKeys.iframeHeight, 0);
state.expectInitializedWith(stateKeys.hasLoaded, false);
state.expectInitializedWith(stateKeys.showError, false);
state.expectInitializedWith(stateKeys.windowTopOffset, null);
});
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(() => {
jest.clearAllMocks();
state.resetVals();
document.getElementById = oldGetElement;
});
it('does not post url hash if the window does not have one', () => {
window.location.hash = '';
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
const cb = getEffects([
props.id,
props.onLoaded,
testIFrameHeight,
true,
], React)[0];
cb();
expect(postMessage).not.toHaveBeenCalled();
});
it('posts url hash if the window has one', () => {
window.location.hash = testHash;
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
const cb = getEffects([
props.id,
props.onLoaded,
testIFrameHeight,
true,
], React)[0];
cb();
expect(postMessage).toHaveBeenCalledWith({ hashName: testHash }, config.LMS_BASE_URL);
});
});
describe('event listener', () => {
it('calls eventListener with prepared callback', () => {
mockState(stateVals);
renderHook(() => useIFrameBehavior(props));
state.mockVals(stateVals);
hook = useIFrameBehavior(props);
const [call] = useEventListener.mock.calls;
expect(call[0]).toEqual('message');
expect(call[1].prereqs).toEqual([
props.id,
props.onLoaded,
stateVals.hasLoaded,
setHasLoaded,
stateVals.iframeHeight,
setIframeHeight,
stateVals.windowTopOffset,
setWindowTopOffset,
state.values.hasLoaded,
state.setState.hasLoaded,
state.values.iframeHeight,
state.setState.iframeHeight,
state.values.windowTopOffset,
state.setState.windowTopOffset,
]);
});
describe('resize message', () => {
const customHeight = 25;
const defaultHeight = 23;
const resizeMessage = (height = defaultHeight) => ({
const resizeMessage = (height = 23) => ({
data: { type: messageTypes.resize, payload: { height } },
});
const videoFullScreenMessage = (open = false) => ({
data: { type: messageTypes.videoFullScreen, payload: { open } },
});
const testSetIFrameHeight = (height = defaultHeight) => {
const testSetIFrameHeight = (height = 23) => {
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage(height));
expect(setIframeHeight).toHaveBeenCalledWith(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();
});
};
describe('hasLoaded', () => {
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);
beforeEach(() => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
});
testOnlySetsHeight();
});
describe('iframeHeight is not 0', () => {
beforeEach(() => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
});
testOnlySetsHeight();
});
describe('payload height is 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);
});
beforeEach(() => { hook = useIFrameBehavior(props); });
testOnlySetsHeight(0);
});
describe('payload is present but uninitialized', () => {
beforeEach(() => {
mockState(defaultStateVals);
});
it('sets iframe height with payload height', () => {
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
testSetIFrameHeight();
});
it('sets hasLoaded and calls onLoaded', () => {
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(setHasLoaded).toHaveBeenCalledWith(true);
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
expect(props.onLoaded).toHaveBeenCalled();
});
test('onLoaded is optional', () => {
renderHook(() => useIFrameBehavior({ ...props, onLoaded: undefined }));
hook = useIFrameBehavior({ ...props, onLoaded: undefined });
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(setHasLoaded).toHaveBeenCalledWith(true);
expect(state.setState.hasLoaded).toHaveBeenCalledWith(true);
});
});
it('scrolls to current window vertical offset if one is set', () => {
const windowTopOffset = 32;
mockState({ ...defaultStateVals, windowTopOffset });
renderHook(() => useIFrameBehavior(props));
state.mockVals({ ...defaultStateVals, windowTopOffset });
hook = 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', () => {
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
cb(resizeMessage());
expect(window.scrollTo).not.toHaveBeenCalled();
@@ -247,16 +259,16 @@ describe('useIFrameBehavior hook', () => {
});
beforeEach(() => {
window.scrollY = scrollY;
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
[[, { cb }]] = useEventListener.mock.calls;
});
it('sets window top offset based on window.scrollY if opening the video', () => {
cb(fullScreenMessage(true));
expect(setWindowTopOffset).toHaveBeenCalledWith(scrollY);
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(scrollY);
});
it('sets window top offset to null if closing the video', () => {
cb(fullScreenMessage(false));
expect(setWindowTopOffset).toHaveBeenCalledWith(null);
expect(state.setState.windowTopOffset).toHaveBeenCalledWith(null);
});
});
describe('offset message', () => {
@@ -268,7 +280,7 @@ describe('useIFrameBehavior hook', () => {
document.getElementById = mockGetEl;
const oldScrollTo = window.scrollTo;
window.scrollTo = jest.fn();
renderHook(() => useIFrameBehavior(props));
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
const offset = 99;
cb({ data: { offset } });
@@ -280,85 +292,18 @@ describe('useIFrameBehavior hook', () => {
});
});
describe('visibility tracking', () => {
it('sets up visibility tracking after iframe loads', () => {
mockState({ ...defaultStateVals, hasLoaded: true });
it('sets up visibility tracking after iframe has loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
useIFrameBehavior(props);
renderHook(() => useIFrameBehavior(props));
const effects = getEffects([true, props.elementId], React);
expect(effects.length).toEqual(2);
effects[0](); // Execute the visibility tracking effect.
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 is handled by the `handleIFrameLoad` method.
expect(postMessage).not.toHaveBeenCalledWith(
expect.objectContaining({ type: 'unit.visibilityStatus' }),
config.LMS_BASE_URL,
);
});
it('does not set up visibility tracking before iframe has loaded', () => {
window.location.hash = ''; // Avoid posting hash message.
mockState({ ...defaultStateVals, hasLoaded: false });
renderHook(() => useIFrameBehavior(props));
expect(global.window.addEventListener).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
});
it('cleans up event listeners on unmount', () => {
mockState({ ...defaultStateVals, hasLoaded: true });
const { unmount } = renderHook(() => useIFrameBehavior(props));
unmount(); // Call the cleanup function.
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
expect(global.window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
});
});
});
describe('output', () => {
describe('handleIFrameLoad', () => {
it('sets and logs error if has not loaded', () => {
mockState(defaultStateVals);
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
expect(setShowError).toHaveBeenCalledWith(true);
expect(logError).toHaveBeenCalled();
});
it('sends track event if has not loaded', () => {
mockState(defaultStateVals);
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
const eventName = 'edx.bi.error.learning.iframe_load_failed';
const eventProperties = {
unitId: props.id,
iframeUrl: props.iframeUrl,
};
expect(sendTrackEvent).toHaveBeenCalledWith(eventName, eventProperties);
});
it('does not set/log errors if loaded', () => {
mockState({ ...defaultStateVals, hasLoaded: true });
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
expect(setShowError).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
});
it('does not send track event if loaded', () => {
mockState({ ...defaultStateVals, hasLoaded: true });
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
expect(sendTrackEvent).not.toHaveBeenCalled();
});
it('registers an event handler to process fetchCourse events.', () => {
mockState(defaultStateVals);
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
const eventName = 'test-event-name';
const event = { data: { event_name: eventName } };
window.onmessage(event);
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
});
it('updates initial iframe visibility on load', () => {
const { result } = renderHook(() => useIFrameBehavior(props));
result.current.handleIFrameLoad();
// Initial visibility update.
expect(postMessage).toHaveBeenCalledWith(
{
type: 'unit.visibilityStatus',
@@ -370,19 +315,81 @@ describe('useIFrameBehavior hook', () => {
config.LMS_BASE_URL,
);
});
it('does not set up visibility tracking before iframe has loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: false });
useIFrameBehavior(props);
const effects = getEffects([false, props.elementId], React);
expect(effects).toBeNull();
expect(global.window.addEventListener).not.toHaveBeenCalled();
expect(postMessage).not.toHaveBeenCalled();
});
it('cleans up event listeners on unmount', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
useIFrameBehavior(props);
const effects = getEffects([true, props.elementId], React);
const cleanup = effects[0](); // Execute the effect and get the cleanup function.
cleanup(); // Call the cleanup function.
expect(global.window.removeEventListener).toHaveBeenCalledTimes(2);
expect(global.window.removeEventListener).toHaveBeenCalledWith('scroll', expect.any(Function));
expect(global.window.removeEventListener).toHaveBeenCalledWith('resize', expect.any(Function));
});
});
});
describe('output', () => {
describe('handleIFrameLoad', () => {
it('sets and logs error if has not loaded', () => {
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
expect(state.setState.showError).toHaveBeenCalledWith(true);
expect(logError).toHaveBeenCalled();
});
it('sends track event if has not loaded', () => {
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
const eventName = 'edx.bi.error.learning.iframe_load_failed';
const eventProperties = {
unitId: props.id,
iframeUrl: props.iframeUrl,
};
expect(sendTrackEvent).toHaveBeenCalledWith(eventName, eventProperties);
});
it('does not set/log errors if loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
expect(state.setState.showError).not.toHaveBeenCalled();
expect(logError).not.toHaveBeenCalled();
});
it('does not send track event if loaded', () => {
state.mockVals({ ...defaultStateVals, hasLoaded: true });
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
expect(sendTrackEvent).not.toHaveBeenCalled();
});
it('registers an event handler to process fetchCourse events.', () => {
hook = useIFrameBehavior(props);
hook.handleIFrameLoad();
const eventName = 'test-event-name';
const event = { data: { event_name: eventName } };
window.onmessage(event);
expect(dispatch).toHaveBeenCalledWith(processEvent(event.data, fetchCourse));
});
});
it('forwards handleIframeLoad, showError, and hasLoaded from state fields', () => {
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);
state.mockVals(stateVals);
hook = useIFrameBehavior(props);
expect(hook.iframeHeight).toEqual(stateVals.iframeHeight);
expect(hook.showError).toEqual(stateVals.showError);
expect(hook.hasLoaded).toEqual(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));
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
const autoAdvanceMessage = () => ({
data: { type: messageTypes.autoAdvance },
@@ -391,10 +398,9 @@ describe('useIFrameBehavior hook', () => {
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));
hook = useIFrameBehavior(props);
const { cb } = useEventListener.mock.calls[0][1];
const autoAdvanceMessage = () => ({
data: { type: messageTypes.autoAdvance },

View File

@@ -1,11 +1,19 @@
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] = React.useState(false);
const [options, setOptions] = React.useState({ height: DEFAULT_HEIGHT });
const [isOpen, setIsOpen] = useKeyedState(stateKeys.isOpen, false);
const [options, setOptions] = useKeyedState(stateKeys.options, { height: DEFAULT_HEIGHT });
const handleModalClose = () => {
const rootFrame = document.querySelector('iframe');

View File

@@ -1,85 +1,74 @@
import React from 'react';
import { renderHook } from '@testing-library/react';
import { mockUseKeyedState } from '@edx/react-unit-test-utils';
import { useEventListener } from '@src/generic/hooks';
import { messageTypes } from '../constants';
import useModalIFrameData, { DEFAULT_HEIGHT } from './useModalIFrameData';
import useModalIFrameData, { stateKeys, 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 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]);
};
const state = mockUseKeyedState(stateKeys);
describe('useModalIFrameData', () => {
beforeEach(() => {
jest.clearAllMocks();
state.mock();
});
const testHandleModalClose = ({ trigger }) => {
const postMessage = jest.fn();
document.querySelector = jest.fn().mockReturnValue({ contentWindow: { postMessage } });
trigger();
expect(React.useState).toHaveBeenNthCalledWith(1, false);
state.expectSetStateCalledWith(stateKeys.isOpen, false);
expect(postMessage).toHaveBeenCalledWith({ type: 'plugin.modal-close' }, '*');
};
describe('behavior', () => {
it('should initialize with modal closed and default height', () => {
const { result } = renderHook(() => useModalIFrameData());
expect(result.current.modalOptions).toEqual({
isOpen: false,
height: DEFAULT_HEIGHT,
});
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 });
});
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', () => {
mockUseStateWithValues({
isOpen: false,
options: oldOptions,
state.mockVals({
[stateKeys.isOpen]: false,
[stateKeys.options]: oldOptions,
});
renderHook(() => useModalIFrameData());
const receiveMessage = prepareListener();
const payload = { test: 'values' };
receiveMessage({ data: { type: messageTypes.modal, payload } });
expect(setIsOpen).toHaveBeenCalledWith(true);
expect(setOptions).toHaveBeenCalled();
const [[setOptionsCb]] = setOptions.mock.calls;
expect(state.setState.isOpen).toHaveBeenCalledWith(true);
expect(state.setState.options).toHaveBeenCalled();
const [[setOptionsCb]] = state.setState.options.mock.calls;
expect(setOptionsCb(oldOptions)).toEqual({ ...oldOptions, ...payload });
});
it('ignores events with no type', () => {
const { result } = renderHook(() => useModalIFrameData());
const initialState = result.current.modalOptions;
state.mockVals({
[stateKeys.isOpen]: false,
[stateKeys.options]: oldOptions,
});
const receiveMessage = prepareListener();
const payload = { test: 'values' };
receiveMessage({ data: { payload } });
expect(result.current.modalOptions).toEqual(initialState);
expect(state.setState.isOpen).not.toHaveBeenCalled();
expect(state.setState.options).not.toHaveBeenCalled();
});
it('calls handleModalClose behavior when receiving a "plugin.modal-close" event', () => {
renderHook(() => useModalIFrameData());
const receiveMessage = prepareListener();
testHandleModalClose({
trigger: () => {
@@ -91,14 +80,13 @@ describe('useModalIFrameData', () => {
});
describe('output', () => {
test('returns handleModalClose callback', () => {
mockUseStateWithValues(defaultState);
testHandleModalClose({ trigger: useModalIFrameData().handleModalClose });
});
it('forwards modalOptions from state values', () => {
const modalOptions = { test: 'options' };
mockUseStateWithValues({
isOpen: true,
options: modalOptions,
state.mockVals({
[stateKeys.options]: modalOptions,
[stateKeys.isOpen]: true,
});
expect(useModalIFrameData().modalOptions).toEqual({
...modalOptions,

View File

@@ -1,13 +1,19 @@
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] = React.useState(false);
const [shouldDisplay, setShouldDisplay] = useKeyedState(stateKeys.shouldDisplay, false);
const { graded } = useModel(modelKeys.units, id);
const { userNeedsIntegritySignature } = useModel(modelKeys.coursewareMeta, courseId);

View File

@@ -1,12 +1,22 @@
import { renderHook } from '@testing-library/react';
import React from 'react';
import { getEffects, mockUseKeyedState } from '@edx/react-unit-test-utils';
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',
@@ -18,29 +28,52 @@ const mockModels = (graded, userNeedsIntegritySignature) => {
));
};
describe('useShouldDisplayHonorCode', () => {
describe('useShouldDisplayHonorCode hook', () => {
beforeEach(() => {
jest.clearAllMocks();
mockModels(false, false);
state.mock();
});
it('should return false when userNeedsIntegritySignature is false', () => {
mockModels(true, false);
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
expect(result.current).toBe(false);
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 graded is false', () => {
mockModels(false, true);
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
expect(result.current).toBe(false);
});
it('should return true when both userNeedsIntegritySignature and graded are true', () => {
mockModels(true, true);
const { result } = renderHook(() => useShouldDisplayHonorCode(props));
expect(result.current).toBe(true);
describe('output', () => {
it('returns shouldDisplay value from state', () => {
const testValue = 'test-value';
state.mockVal(stateKeys.shouldDisplay, testValue);
expect(useShouldDisplayHonorCode(props)).toEqual(testValue);
});
});
});

View File

@@ -0,0 +1,17 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`DecodePageRoute should not modify the url if it does not need to be decoded 1`] = `
<div>
PageWrap: {
"children": [
" ",
[
" ",
[],
" "
],
" "
]
}
</div>
`;

View File

@@ -62,10 +62,11 @@ describe('DecodePageRoute', () => {
const props = matchPath({
path: '/course/:courseId/home',
}, `/course/${decodedCourseId}/home`);
renderPage(props);
const { container } = 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', () => {

View File

@@ -166,13 +166,11 @@ subscribe(APP_INIT_ERROR, (error) => {
initialize({
handlers: {
config: () => {
/* istanbul ignore next */
mergeConfig({
CONTACT_URL: process.env.CONTACT_URL || null,
CREDENTIALS_BASE_URL: process.env.CREDENTIALS_BASE_URL || null,
CREDIT_HELP_LINK_URL: process.env.CREDIT_HELP_LINK_URL || null,
DISCUSSIONS_MFE_BASE_URL: process.env.DISCUSSIONS_MFE_BASE_URL || null,
DISCOUNT_CODE_INFO_URL: process.env.DISCOUNT_CODE_INFO_URL || null,
ENTERPRISE_LEARNER_PORTAL_HOSTNAME: process.env.ENTERPRISE_LEARNER_PORTAL_HOSTNAME || null,
ENTERPRISE_LEARNER_PORTAL_URL: process.env.ENTERPRISE_LEARNER_PORTAL_URL || null,
ENABLE_JUMPNAV: process.env.ENABLE_JUMPNAV || null,

View File

@@ -69,11 +69,15 @@ 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.', () => {
test('subscribe: APP_INIT_ERROR. snapshot: displays an ErrorPage to root element', () => {
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();
});
});

View File

@@ -1,4 +1,4 @@
# Content IFrame Loader Slot
# Content iframe Loader Slot
### Slot ID: `org.openedx.frontend.learning.content_iframe_loader.v1`
@@ -6,51 +6,5 @@
* `content_iframe_loader_slot`
### Props:
* `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.
![Content Iframe Loader Example](./images/loader-example.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.learning.content_iframe_loader.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widgetId: 'default_contents',
widget: {
id: 'custom_iframe_loader',
type: DIRECT_PLUGIN,
RenderWidget: ({ courseId, defaultLoaderComponent }) => (
<div style={{ textAlign: 'center', padding: '2rem' }}>
<h3>Loading course content...</h3>
<p>Course: {courseId}</p>
<div style={{ margin: '1rem 0' }}>
{defaultLoaderComponent}
</div>
<p>Please wait while we prepare your learning experience</p>
</div>
),
},
},
]
}
},
}
export default config;
```
* `courseId`
* `defaultLoaderComponent`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 80 KiB

View File

@@ -6,55 +6,5 @@
* `outline_tab_notifications_slot`
### Props:
* `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.
![Course notification example](./images/course-outline-notification-example.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.learning.course_outline_tab_notifications.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'custom_outline_notification',
type: DIRECT_PLUGIN,
RenderWidget: ({ courseId, model }) => (
<div className="card mb-3">
<div className="card-body">
<h5 className="card-title">📢 Course Announcement</h5>
<p className="card-text">
Important updates for course {courseId}
</p>
<p className="text-muted small">
Context: {model}
</p>
<button className="btn btn-primary btn-sm">
View Details
</button>
</div>
</div>
),
},
},
]
}
},
}
export default config;
```
* `courseId`
* `model`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 135 KiB

View File

@@ -6,65 +6,4 @@
* `gated_unit_content_message_slot`
### Props:
* `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.
![Gated unit message example](./images/gated-unit-message-example.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.learning.gated_unit_content_message.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widgetId: 'default_contents',
widget: {
id: 'custom_gated_message',
type: DIRECT_PLUGIN,
RenderWidget: ({ courseId }) => (
<div className="alert alert-warning" role="alert">
<div className="d-flex align-items-center mb-3">
<i className="fa fa-lock fa-2x me-3" aria-hidden="true"></i>
<div>
<h4 className="alert-heading mb-1">Premium Content</h4>
<p className="mb-0">This content is available to verified learners only.</p>
</div>
</div>
<hr />
<p className="mb-3">
Upgrade your enrollment for course {courseId} to access:
</p>
<ul className="mb-3">
<li> Graded assignments and quizzes</li>
<li>🏆 Verified certificate upon completion</li>
<li>💬 Full discussion forum access</li>
<li>📱 Mobile app offline access</li>
</ul>
<button className="btn btn-success">
Upgrade Now
</button>
</div>
),
},
},
]
}
},
}
export default config;
```
* `courseId`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

View File

@@ -6,92 +6,6 @@
* `notification_tray_slot`
### Props:
* `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.
![Notification tray slot example](./images/notification-tray-slot-example.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const config = {
pluginSlots: {
'org.openedx.frontend.learning.notification_tray.v1': {
plugins: [
{
// Insert custom notification content
op: PLUGIN_OPERATIONS.Replace,
widget: {
id: 'custom_notifications',
type: DIRECT_PLUGIN,
RenderWidget: ({
courseId,
model,
notificationCurrentState,
setNotificationCurrentState
}) => (
<div className="p-3">
<h5 className="mb-3">📬 Course Notifications</h5>
{notificationCurrentState === 'accessLastHour' && (
<div className="alert alert-warning mb-3">
<strong> Last Chance!</strong>
<p className="mb-0">Your access expires in less than an hour.</p>
</div>
)}
{notificationCurrentState === 'accessDaysLeft' && (
<div className="alert alert-info mb-3">
<strong>📅 Access Reminder</strong>
<p className="mb-0">Your course access expires in a few days.</p>
</div>
)}
<div className="notification-item p-2 mb-2 border rounded">
<div className="d-flex justify-content-between align-items-center">
<span>📚 New course material available</span>
<button
className="btn btn-sm btn-outline-primary"
onClick={() => setNotificationCurrentState('accessDateView')}
>
View
</button>
</div>
</div>
<div className="notification-item p-2 mb-2 border rounded">
<div className="d-flex justify-content-between align-items-center">
<span>🎯 Assignment due tomorrow</span>
<span className="badge bg-warning">Due Soon</span>
</div>
</div>
<div className="text-center mt-3">
<small className="text-muted">
Course: {courseId} | Model: {model}
</small>
</div>
</div>
),
},
},
]
}
},
}
export default config;
```
* `courseId`
* `notificationCurrentState`
* `setNotificationCurrentState`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 118 KiB

View File

@@ -6,125 +6,8 @@
* `notification_widget_slot`
### Props:
* `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.
![Notification widget slot example](./images/notification-widget-slot-example.png)
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
const NotificationWidget = ({
courseId,
model,
notificationCurrentState,
setNotificationCurrentState,
toggleSidebar
}) => {
const getNotificationContent = () => {
switch (notificationCurrentState) {
case 'accessLastHour':
return {
icon: '⚠️',
title: 'Final Hour!',
message: 'Access expires in less than 1 hour',
variant: 'danger'
};
case 'accessHoursLeft':
return {
icon: '⏰',
title: 'Expiring Soon',
message: 'Access expires in a few hours',
variant: 'warning'
};
case 'accessDaysLeft':
return {
icon: '📅',
title: 'Access Reminder',
message: 'Access expires in a few days',
variant: 'info'
};
case 'FPDdaysLeft':
case 'FPDLastHour':
return {
icon: '🎯',
title: 'Upgrade Available',
message: 'Get full access to premium features',
variant: 'primary'
};
default:
return {
icon: '🔔',
title: 'Notifications',
message: 'Click to view updates',
variant: 'secondary'
};
}
};
const notification = getNotificationContent();
return (
<div
className={`alert alert-${notification.variant} mb-0 cursor-pointer`}
onClick={toggleSidebar}
role="button"
tabIndex={0}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
toggleSidebar();
}
}}
>
<div className="d-flex align-items-center">
<span className="me-2" style={{ fontSize: '1.2em' }}>
{notification.icon}
</span>
<div className="flex-fill">
<strong className="d-block">{notification.title}</strong>
<small>{notification.message}</small>
</div>
<div className="ms-2">
<i className="fa fa-chevron-right" aria-hidden="true"></i>
</div>
</div>
</div>
);
}
const config = {
pluginSlots: {
'org.openedx.frontend.learning.notification_widget.v1': {
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widgetId: 'default_contents',
widget: {
id: 'custom_notification_widget',
type: DIRECT_PLUGIN,
RenderWidget: NotificationWidget
},
},
]
}
},
}
export default config;
```
* `courseId`
* `model`
* `notificationCurrentState`
* `setNotificationCurrentState`
* `toggleSidebar`

Binary file not shown.

Before

Width:  |  Height:  |  Size: 91 KiB

View File

@@ -1,11 +1,11 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
export const getUnsubscribeUrl = (userToken) => (
`${getConfig().LMS_BASE_URL}/api/notifications/preferences/update/${userToken}/`
export const getUnsubscribeUrl = (userToken, updatePatch) => (
`${getConfig().LMS_BASE_URL}/api/notifications/preferences/update/${userToken}/${updatePatch}/`
);
export async function unsubscribeNotificationPreferences(userToken) {
const url = getUnsubscribeUrl(userToken);
export async function unsubscribeNotificationPreferences(userToken, updatePatch) {
const url = getUnsubscribeUrl(userToken, updatePatch);
return getAuthenticatedHttpClient().get(url);
}

View File

@@ -17,18 +17,18 @@ import messages from './messages';
const PreferencesUnsubscribe = () => {
const intl = useIntl();
const { userToken } = useParams();
const { userToken, updatePatch } = useParams();
const [status, setStatus] = useState(LOADING);
useEffect(() => {
unsubscribeNotificationPreferences(userToken).then(
unsubscribeNotificationPreferences(userToken, updatePatch).then(
() => setStatus(LOADED),
(error) => {
setStatus(FAILED);
logError(error);
},
);
sendTrackEvent('edx.ui.lms.notifications.preferences.unsubscribe', { userToken });
sendTrackEvent('edx.ui.lms.notifications.preferences.unsubscribe', { userToken, updatePatch });
}, []);
const pageContent = {

View File

@@ -24,7 +24,8 @@ describe('Notification Preferences One Click Unsubscribe', () => {
let component;
let store;
const userToken = '1234';
const url = getUnsubscribeUrl(userToken);
const updatePatch = 'abc123';
const url = getUnsubscribeUrl(userToken, updatePatch);
beforeAll(async () => {
await initializeTestStore();
@@ -38,7 +39,7 @@ describe('Notification Preferences One Click Unsubscribe', () => {
component = (
<AppProvider store={store} wrapWithRouter={false}>
<UserMessagesProvider>
<MemoryRouter initialEntries={[`${`/preferences-unsubscribe/${userToken}/`}`]}>
<MemoryRouter initialEntries={[`${`/preferences-unsubscribe/${userToken}/${updatePatch}/`}`]}>
<Routes>
<Route path={ROUTES.PREFERENCES_UNSUBSCRIBE} element={<PreferencesUnsubscribe />} />
</Routes>
@@ -68,6 +69,7 @@ describe('Notification Preferences One Click Unsubscribe', () => {
expect(screen.getByTestId('heading-text')).toHaveTextContent('Error unsubscribing from preference');
expect(sendTrackEvent).toHaveBeenCalledWith('edx.ui.lms.notifications.preferences.unsubscribe', {
userToken,
updatePatch,
});
});
});

View File

@@ -1,8 +1,9 @@
/* eslint-disable react/prop-types */
import React, { useEffect, useState } from 'react';
import PropTypes from 'prop-types';
import { getConfig } from '@edx/frontend-platform';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Lightbulb, MoneyFilled } from '@openedx/paragon/icons';
import {
@@ -15,12 +16,7 @@ import { useModel } from '../../generic/model-store';
import StreakMobileImage from './assets/Streak_mobile.png';
import StreakDesktopImage from './assets/Streak_desktop.png';
import messages from './messages';
import {
calculateVoucherDiscountPercentage,
getDiscountCodePercentage,
recordModalClosing,
recordStreakCelebration,
} from './utils';
import { recordModalClosing, recordStreakCelebration } from './utils';
function getRandomFactoid(intl, streakLength) {
const boldedSectionA = intl.formatMessage(messages.streakFactoidABoldedSection);
@@ -46,6 +42,13 @@ function getRandomFactoid(intl, streakLength) {
return factoids[Math.floor(Math.random() * (factoids.length))];
}
async function calculateVoucherDiscount(voucher, sku, username) {
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
return getAuthenticatedHttpClient().get(url)
.then(res => camelCaseObject(res));
}
const CloseText = ({ intl }) => (
<span>
{intl.formatMessage(messages.streakButton)}
@@ -80,38 +83,34 @@ const StreakModal = ({
// Ask ecommerce to calculate discount savings
useEffect(() => {
(async () => {
let streakDiscountPercentage = 0;
try {
if (streakDiscountCouponEnabled && verifiedMode) {
// If the discount service is available, use it to get the discount percentage
if (getConfig().DISCOUNT_CODE_INFO_URL) {
streakDiscountPercentage = await getDiscountCodePercentage(
discountCode,
courseId,
);
// If the discount service is not available, fall back to ecommerce to calculate the discount percentage
} else if (getConfig().ECOMMERCE_BASE_URL) {
streakDiscountPercentage = await calculateVoucherDiscountPercentage(
discountCode,
verifiedMode.sku,
username,
);
}
}
} catch {
// ignore any errors - we just won't show the discount to the user then
} finally {
if (streakDiscountPercentage) {
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
course_id: courseId,
sku: verifiedMode.sku,
});
}
setDiscountPercent(streakDiscountPercentage);
}
})();
// eslint-disable-next-line react-hooks/exhaustive-deps
if (streakDiscountCouponEnabled && verifiedMode && getConfig().ECOMMERCE_BASE_URL) {
calculateVoucherDiscount(discountCode, verifiedMode.sku, username)
.then(
(result) => {
const { totalInclTax, totalInclTaxExclDiscounts } = result.data;
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
// multiplied by the calculated percentage.
setDiscountPercent(1 - totalInclTax / totalInclTaxExclDiscounts);
sendTrackEvent('edx.bi.course.streak_discount_enabled', {
course_id: courseId,
sku: verifiedMode.sku,
});
} else {
setDiscountPercent(0);
}
},
() => {
// ignore any errors - we just won't show the discount to the user then
setDiscountPercent(0);
},
);
} else {
setDiscountPercent(0);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [streakDiscountCouponEnabled, username, verifiedMode]);
if (!isStreakCelebrationOpen) {

View File

@@ -1,6 +1,6 @@
import React from 'react';
import { Factory } from 'rosie';
import { camelCaseObject, getConfig, mergeConfig } from '@edx/frontend-platform';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { breakpoints } from '@openedx/paragon';
@@ -34,19 +34,6 @@ describe('Loaded Tab Page', () => {
});
}
function setDiscountViaDiscountCodeInfo(percent) {
const discountURLParams = new URLSearchParams();
discountURLParams.append('code', 'ZGY11119949');
discountURLParams.append('course_run_key', courseMetadata.id);
const discountURL = `${getConfig().DISCOUNT_CODE_INFO_URL}?${discountURLParams.toString()}`;
mockData.streakDiscountCouponEnabled = true;
axiosMock.onGet(discountURL).reply(200, {
isApplicable: true,
discountPercentage: percent / 100,
});
}
function setDiscountError() {
mockData.streakDiscountCouponEnabled = true;
axiosMock.onGet(calculateUrl).reply(500);
@@ -118,22 +105,4 @@ describe('Loaded Tab Page', () => {
sku: mockData.verifiedMode.sku,
});
});
it('shows discount version of streak celebration modal when discount available and info fetched using DISCOUNT_CODE_INFO_URL', async () => {
mergeConfig({ DISCOUNT_CODE_INFO_URL: 'http://localhost:8140/lms/discount-code-info/' });
global.innerWidth = breakpoints.extraSmall.maxWidth;
setDiscountViaDiscountCodeInfo(14);
await renderModal();
const endDateText = `Ends ${new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toLocaleDateString({ timeZone: 'UTC' })}.`;
expect(screen.getByText('Youve unlocked a 14% off discount when you upgrade this course for a limited time only.', { exact: false })).toBeInTheDocument();
expect(screen.getByText(endDateText, { exact: false })).toBeInTheDocument();
expect(screen.getByText('Continue with course')).toBeInTheDocument();
expect(screen.queryByText('Keep it up')).not.toBeInTheDocument();
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.course.streak_discount_enabled', {
course_id: mockData.courseId,
sku: mockData.verifiedMode.sku,
});
});
});

View File

@@ -1,9 +1,5 @@
import { sendTrackEvent } from '@edx/frontend-platform/analytics';
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import {
getAuthenticatedHttpClient,
getAuthenticatedUser,
} from '@edx/frontend-platform/auth';
import { getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { updateModel } from '../../generic/model-store';
@@ -28,39 +24,4 @@ function recordModalClosing(celebrations, org, courseId, dispatch) {
}));
}
async function calculateVoucherDiscountPercentage(voucher, sku, username) {
const urlBase = `${getConfig().ECOMMERCE_BASE_URL}/api/v2/baskets/calculate`;
const url = `${urlBase}/?code=${voucher}&sku=${sku}&username=${username}`;
const result = await getAuthenticatedHttpClient().get(url);
const { totalInclTax, totalInclTaxExclDiscounts } = camelCaseObject(result).data;
if (totalInclTaxExclDiscounts && totalInclTax !== totalInclTaxExclDiscounts) {
// Just store the percent (rather than using these values directly), because ecommerce doesn't give us
// the currency symbol to use, so we want to use the symbol that LMS gives us. And I don't want to assume
// ecommerce's currency is the same as the LMS. So we'll keep using the values in verifiedMode, just
// multiplied by the calculated percentage.
return 1 - totalInclTax / totalInclTaxExclDiscounts;
}
return 0;
}
async function getDiscountCodePercentage(code, courseId) {
const params = new URLSearchParams();
params.append('code', code);
params.append('course_run_key', courseId);
const url = `${getConfig().DISCOUNT_CODE_INFO_URL}?${params.toString()}`;
const result = await getAuthenticatedHttpClient().get(url);
const { isApplicable, discountPercentage } = camelCaseObject(result).data;
return isApplicable ? +discountPercentage : 0;
}
export {
calculateVoucherDiscountPercentage,
getDiscountCodePercentage,
recordModalClosing,
recordStreakCelebration,
};
export { recordStreakCelebration, recordModalClosing };