Compare commits

..

1 Commits

Author SHA1 Message Date
dependabot[bot]
a8f90cfc1b build(deps): bump actions/setup-node from 4 to 6
Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](https://github.com/actions/setup-node/compare/v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-10-20 03:19:23 +00:00
160 changed files with 6795 additions and 9747 deletions

View File

@@ -26,46 +26,31 @@ This is a micro-frontend application responsible for the login, registration and
Getting Started
***************
Prerequisites
=============
Installation
============
`Tutor`_ is currently recommended as a development environment for your new MFE. Please refer to the `relevant tutor-mfe documentation`_ to get started using it.
.. _Tutor: https://github.com/overhangio/tutor
.. _relevant tutor-mfe documentation: https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development
Cloning and Startup
===================
Devstack (Deprecated) instructions
==================================
1. Clone your new repo:
1. Install Devstack using the `Getting Started <https://github.com/openedx/devstack#getting-started>`_ instructions.
.. code-block:: bash
2. Start up LMS, if it's not already started.
git clone https://github.com/edx/frontend-app-authn.git
4. Within this project (frontend-app-authn), install requirements and start the development server:
2. Use the version of Node specified in the ``.nvmrc`` file.
.. code-block::
The current version of the micro-frontend build scripts supports the version of Node found in ``.nvmrc``.
Using other major versions of node *may* work, but this is unsupported. For
convenience, this repository includes a ``.nvmrc`` file to help in setting the
correct node version via `nvm <https://github.com/nvm-sh/nvm>`_.
npm install
npm start # The server will run on port 1999
3. Install npm dependencies:
5. Once the dev server is up, visit http://localhost:1999 to access the MFE
.. code-block:: bash
cd frontend-app-authn && npm install
4. Update the application port to use for local development:
The default port is 1999. If this does not work for you, update the line
``PORT=1999`` to your port in all ``.env.*`` files
5. Start the devserver. The app will be running at ``localhost:1999``, or whatever port you change it too.
.. code-block:: bash
npm run dev
.. image:: ./docs/images/frontend-app-authn-localhost-preview.png
**Note:** Follow `Enable social auth locally <docs/how_tos/enable_social_auth.rst>`_ for enabling Social Sign-on Buttons (SSO) locally
@@ -130,7 +115,7 @@ The authentication micro-frontend also requires the following additional variabl
* - ``MFE_CONFIG_API_URL``
- Link of the API to get runtime mfe configuration variables from the site configuration or django settings.
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
- ``/api/v1/mfe_config`` | ``''`` (empty strings are falsy)
* - ``APP_ID``
- Name of MFE, this will be used by the API to get runtime configurations for the specific micro frontend. For a frontend repo `frontend-app-appName`, use `appName` as APP_ID.
@@ -160,7 +145,7 @@ Furthermore, there are several edX-specific environment variables that enable in
* - ``SHOW_CONFIGURABLE_EDX_FIELDS``
- For edX, country and honor code fields are required by default. This flag enables edX specific required fields.
- ``true`` | ``''`` (empty strings are falsy)
- ``true`` | ``''`` (empty strings are falsy)
For more information see the document: `Micro-frontend applications in Open
edX <https://github.com/overhangio/tutor-mfe?tab=readme-ov-file#mfe-development>`__.

3253
package-lock.json generated
View File

@@ -16,10 +16,9 @@
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@tanstack/react-query": "^5.90.19",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.26.0",
@@ -33,111 +32,109 @@
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.9.1",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.3.0",
"jest": "30.2.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
}
},
"node_modules/@adobe/css-tools": {
"version": "4.4.4",
"resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz",
"integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==",
"dev": true,
"license": "MIT"
},
"node_modules/@algolia/cache-browser-local-storage": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.27.0.tgz",
"integrity": "sha512-YGog2s57sO20lvpa+hv5XLAAmiTI1kHsCMRtPVfiaOdIQnvRla21lfH08onqEbZihOPVI8GULwt79zQB2ymKzg==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/cache-browser-local-storage/-/cache-browser-local-storage-4.25.2.tgz",
"integrity": "sha512-tA1rqAafI+gUdewjZwyTsZVxesl22MTgLWRKt1+TBiL26NiKx7SjRqTI3pzm8ngx1ftM5LSgXkVIgk2+SRgPTg==",
"license": "MIT",
"dependencies": {
"@algolia/cache-common": "4.27.0"
"@algolia/cache-common": "4.25.2"
}
},
"node_modules/@algolia/cache-common": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.27.0.tgz",
"integrity": "sha512-Sr8zjNXj82p6lO4W9CdzfF0m0/9h/H6VAdSHOTtimm/cTzXIYXRI2IZq7+Nt2ljJ7Ukx+7dIFcxQjE57eQSPsw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/cache-common/-/cache-common-4.25.2.tgz",
"integrity": "sha512-E+aZwwwmhvZXsRA1+8DhH2JJIwugBzHivASTnoq7bmv0nmForLyH7rMG5cOTiDK36DDLnKq1rMGzxWZZ70KZag==",
"license": "MIT"
},
"node_modules/@algolia/cache-in-memory": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.27.0.tgz",
"integrity": "sha512-abgMRTcVD0IllNvMM9JFhxtyLn1v6Ey7mQ7+BGS3JCzvkCX7KZqlS0BIuVUDgx9sPIfOeNsG/awGzMmP50TwZw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/cache-in-memory/-/cache-in-memory-4.25.2.tgz",
"integrity": "sha512-KYcenhfPKgR+WJ6IEwKVEFMKKCWLZdnYuw08+3Pn1cxAXbJcTIKjoYgEXzEW6gJmDaau2l55qNrZo6MBbX7+sw==",
"license": "MIT",
"dependencies": {
"@algolia/cache-common": "4.27.0"
"@algolia/cache-common": "4.25.2"
}
},
"node_modules/@algolia/client-account": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.27.0.tgz",
"integrity": "sha512-sSHxwrKTKJrwfoR/LcQJZfmiWJcM5d9Rp7afMChxOcdGdkSdIwrNBC8SCcHRenA3GsZ6mg+j6px7KWYxJ34btA==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/client-account/-/client-account-4.25.2.tgz",
"integrity": "sha512-IfRGhBxvjli9mdexrCxX2N4XT9NBN3tvZK5zCaL8zkDcgsthiM9WPvGIZS/pl/FuXB7hA0lE5kqOzsQDP6OmGQ==",
"license": "MIT",
"dependencies": {
"@algolia/client-common": "4.27.0",
"@algolia/client-search": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/client-common": "4.25.2",
"@algolia/client-search": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/client-analytics": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.27.0.tgz",
"integrity": "sha512-MqIDyxODljn9ZC4oqjQD0kez2a4zjIJ9ywA/b7cIiUiK/tDjZNTVjYd9WXMKQlXnWUwfrfXJZClVVqN1iCXS+Q==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-4.25.2.tgz",
"integrity": "sha512-4Yxxhxh+XjXY8zPyo+h6tQuyoJWDBn8E3YLr8j+YAEy5p+r3/5Tp+ANvQ+hNaQXbwZpyf5d4ViYOBjJ8+bWNEg==",
"license": "MIT",
"dependencies": {
"@algolia/client-common": "4.27.0",
"@algolia/client-search": "4.27.0",
"@algolia/requester-common": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/client-common": "4.25.2",
"@algolia/client-search": "4.25.2",
"@algolia/requester-common": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/client-common": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.27.0.tgz",
"integrity": "sha512-ZrT6l/YPQgyIUuBCxcYPeXol2VBLUMuNb1rKXrm6z1f/iTiwqtnEEb16/6CC11+Re0ZGXrdcMVrgDRrzveQ1aQ==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-4.25.2.tgz",
"integrity": "sha512-HXX8vbJPYW29P18GxciiwaDpQid6UhpPP9nW9WE181uGUgFhyP5zaEkYWf9oYBrjMubrGwXi5YEzJOz6Oa4faA==",
"license": "MIT",
"dependencies": {
"@algolia/requester-common": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/requester-common": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/client-personalization": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.27.0.tgz",
"integrity": "sha512-OZqaFFVm+10hAlmxpiTWi/o2n+YKBESbSqSy2yXAumPH/kaK4moJHFblbh8IkV3KZR0lLm4hzPtn8Q2nWNiDUA==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-4.25.2.tgz",
"integrity": "sha512-K81PRaHF77mHv2u8foWTHnIf5c+QNf/SnKNM7rB8JPi7TMYi4E5o2mFbgdU1ovd8eg9YMOEAuLkl1Nz1vbM3zQ==",
"license": "MIT",
"dependencies": {
"@algolia/client-common": "4.27.0",
"@algolia/requester-common": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/client-common": "4.25.2",
"@algolia/requester-common": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/client-search": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.27.0.tgz",
"integrity": "sha512-qmX/f67ay0eZ4V5Io8fWWOcUVo/gqre2yei1PnmEhQU2Gul6ushg25QnNrfu4BODiRrw1rwYveZaLCiHvcUxrQ==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-4.25.2.tgz",
"integrity": "sha512-pO/LpVnQlbJpcHRk+AroWyyFnh01eOlO6/uLZRUmYvr/hpKZKxI6n7ufgTawbo0KrAu2CePfiOkStYOmDuRjzQ==",
"license": "MIT",
"dependencies": {
"@algolia/client-common": "4.27.0",
"@algolia/requester-common": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/client-common": "4.25.2",
"@algolia/requester-common": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/events": {
@@ -147,72 +144,72 @@
"license": "MIT"
},
"node_modules/@algolia/logger-common": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.27.0.tgz",
"integrity": "sha512-pIrmQRXtDV+zTMVXKtKCosC2rWhn0F0TdUeb9etA6RiAz6jY6bY6f0+JX7YekDK09SnmZMLIyUa7Jci+Ied9bw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/logger-common/-/logger-common-4.25.2.tgz",
"integrity": "sha512-aUXpcodoIpLPsnVc2OHgC9E156R7yXWLW2l+Zn24Cyepfq3IvmuVckBvJDpp7nPnXkEzeMuvnVxQfQsk+zP/BA==",
"license": "MIT"
},
"node_modules/@algolia/logger-console": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.27.0.tgz",
"integrity": "sha512-UWvta8BxsR/u5z9eI088mOSLQaGtmoCtXeN3DYJurlxAdJwPuKtEb5+433kxA6/E9f2/JgoW89KZ1vNP9pcHBQ==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/logger-console/-/logger-console-4.25.2.tgz",
"integrity": "sha512-H3Y+UB0Ty0htvMJ6zDSufhFTSDlg3Pyj3AXilfDdDRcvfhH4C/cJNVm+CTaGORxL5uKABGsBp+SZjsEMTyAunQ==",
"license": "MIT",
"dependencies": {
"@algolia/logger-common": "4.27.0"
"@algolia/logger-common": "4.25.2"
}
},
"node_modules/@algolia/recommend": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.27.0.tgz",
"integrity": "sha512-CFy54xDjrsazPi3KN04yPmLRDT72AKokc3RLOdWQvG0/uEUjj7dhWqe9qenxpL4ydsjO7S1eY5YqmX+uMGonlg==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-4.25.2.tgz",
"integrity": "sha512-puRrGeXwAuVa4mLdvXvmxHRFz9MkcCOLPcjz7MjU4NihlpIa+lZYgikJ7z0SUAaYgd6l5Bh00hXiU/OlX5ffXQ==",
"license": "MIT",
"dependencies": {
"@algolia/cache-browser-local-storage": "4.27.0",
"@algolia/cache-common": "4.27.0",
"@algolia/cache-in-memory": "4.27.0",
"@algolia/client-common": "4.27.0",
"@algolia/client-search": "4.27.0",
"@algolia/logger-common": "4.27.0",
"@algolia/logger-console": "4.27.0",
"@algolia/requester-browser-xhr": "4.27.0",
"@algolia/requester-common": "4.27.0",
"@algolia/requester-node-http": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/cache-browser-local-storage": "4.25.2",
"@algolia/cache-common": "4.25.2",
"@algolia/cache-in-memory": "4.25.2",
"@algolia/client-common": "4.25.2",
"@algolia/client-search": "4.25.2",
"@algolia/logger-common": "4.25.2",
"@algolia/logger-console": "4.25.2",
"@algolia/requester-browser-xhr": "4.25.2",
"@algolia/requester-common": "4.25.2",
"@algolia/requester-node-http": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/@algolia/requester-browser-xhr": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.27.0.tgz",
"integrity": "sha512-dTenMBIIpyp5o3C2ZnfbsuSlD/lL9jPkk6T+2+qm38fyw2nf49ANbcHFE79NgiGrnmw7QrYveCs9NIP3Wk4v6g==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-4.25.2.tgz",
"integrity": "sha512-aAjfsI0AjWgXLh/xr9eoR8/9HekBkIER3bxGoBf9d1XWMMoTo/q92Da2fewkxwLE6mla95QJ9suJGOtMOewXXQ==",
"license": "MIT",
"dependencies": {
"@algolia/requester-common": "4.27.0"
"@algolia/requester-common": "4.25.2"
}
},
"node_modules/@algolia/requester-common": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.27.0.tgz",
"integrity": "sha512-VC3prAQVgWTubMStb3mJz6i61Hqbtagi2LeIbgNtoFJFff3XZDcAaO1D5r0GYl2+DrB2VzUHnQXbkiuI+HHYyg==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/requester-common/-/requester-common-4.25.2.tgz",
"integrity": "sha512-Q4wC3sgY0UFjV3Rb3icRLTpPB5/M44A8IxzJHM9PNeK1T3iX7X/fmz7ATUYQYZTpwHCYATlsQKWiTpql1hHjVg==",
"license": "MIT"
},
"node_modules/@algolia/requester-node-http": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.27.0.tgz",
"integrity": "sha512-y8nUqaUQeSOQ5oaNo0b2QPznyBFW9LoIwljyUphJ+gUZpU6O/j2/C8ovoqDpIe6J0etqHg5RCcBizrCFZuLpyw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-4.25.2.tgz",
"integrity": "sha512-Ja/FYB7W9ZM+m8UrMIlawNUAKpncvb9Mo+D8Jq5WepGTUyQ9CBYLsjwxv9O8wbj3TSWqTInf4uUBJ2FKR8G7xw==",
"license": "MIT",
"dependencies": {
"@algolia/requester-common": "4.27.0"
"@algolia/requester-common": "4.25.2"
}
},
"node_modules/@algolia/transporter": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.27.0.tgz",
"integrity": "sha512-PvSbELU4VjN3xSX79ki+zIdOGhTxyJXWvRDzkUjfTx2iNfPWDdTjzKbP1o+268coJztxrkuBwJz90Urek7o1Kw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/@algolia/transporter/-/transporter-4.25.2.tgz",
"integrity": "sha512-yw3RLHWc6V+pbdsFtq8b6T5bJqLDqnfKWS7nac1Vzcmgvs/V/Lfy7/6iOF9XRilu5aBDOBHoP1SOeIDghguzWw==",
"license": "MIT",
"dependencies": {
"@algolia/cache-common": "4.27.0",
"@algolia/logger-common": "4.27.0",
"@algolia/requester-common": "4.27.0"
"@algolia/cache-common": "4.25.2",
"@algolia/logger-common": "4.25.2",
"@algolia/requester-common": "4.25.2"
}
},
"node_modules/@ampproject/remapping": {
@@ -260,12 +257,12 @@
}
},
"node_modules/@babel/code-frame": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz",
"integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz",
"integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==",
"license": "MIT",
"dependencies": {
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/helper-validator-identifier": "^7.27.1",
"js-tokens": "^4.0.0",
"picocolors": "^1.1.1"
},
@@ -274,9 +271,9 @@
}
},
"node_modules/@babel/compat-data": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz",
"integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz",
"integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==",
"devOptional": true,
"license": "MIT",
"engines": {
@@ -289,6 +286,7 @@
"integrity": "sha512-5e3FI4Q3M3Pbr21+5xJwCv6ZT6KmGkI0vw3Tozy5ODAQFTIWe37iT8Cr7Ice2Ntb+M3iSKCEWMB1MBgKrW3whg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@ampproject/remapping": "^2.2.0",
"@babel/code-frame": "^7.24.7",
@@ -334,14 +332,14 @@
}
},
"node_modules/@babel/generator": {
"version": "7.29.1",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz",
"integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz",
"integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/parser": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.28.3",
"@babel/types": "^7.28.2",
"@jridgewell/gen-mapping": "^0.3.12",
"@jridgewell/trace-mapping": "^0.3.28",
"jsesc": "^3.0.2"
@@ -364,13 +362,13 @@
}
},
"node_modules/@babel/helper-compilation-targets": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz",
"integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz",
"integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/compat-data": "^7.28.6",
"@babel/compat-data": "^7.27.2",
"@babel/helper-validator-option": "^7.27.1",
"browserslist": "^4.24.0",
"lru-cache": "^5.1.1",
@@ -462,29 +460,29 @@
}
},
"node_modules/@babel/helper-module-imports": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz",
"integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz",
"integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/traverse": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/traverse": "^7.27.1",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/helper-module-transforms": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz",
"integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==",
"version": "7.28.3",
"resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz",
"integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-module-imports": "^7.28.6",
"@babel/helper-validator-identifier": "^7.28.5",
"@babel/traverse": "^7.28.6"
"@babel/helper-module-imports": "^7.27.1",
"@babel/helper-validator-identifier": "^7.27.1",
"@babel/traverse": "^7.28.3"
},
"engines": {
"node": ">=6.9.0"
@@ -577,9 +575,9 @@
}
},
"node_modules/@babel/helper-validator-identifier": {
"version": "7.28.5",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz",
"integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==",
"version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz",
"integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==",
"license": "MIT",
"engines": {
"node": ">=6.9.0"
@@ -611,27 +609,27 @@
}
},
"node_modules/@babel/helpers": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
"integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz",
"integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/template": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/parser": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz",
"integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz",
"integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/types": "^7.29.0"
"@babel/types": "^7.28.4"
},
"bin": {
"parser": "bin/babel-parser.js"
@@ -2142,33 +2140,33 @@
}
},
"node_modules/@babel/template": {
"version": "7.28.6",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz",
"integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==",
"version": "7.27.2",
"resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz",
"integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.28.6",
"@babel/parser": "^7.28.6",
"@babel/types": "^7.28.6"
"@babel/code-frame": "^7.27.1",
"@babel/parser": "^7.27.2",
"@babel/types": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/traverse": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz",
"integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz",
"integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-globals": "^7.28.0",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/types": "^7.29.0",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/types": "^7.28.4",
"debug": "^4.3.1"
},
"engines": {
@@ -2176,14 +2174,14 @@
}
},
"node_modules/@babel/types": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz",
"integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==",
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz",
"integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"@babel/helper-string-parser": "^7.27.1",
"@babel/helper-validator-identifier": "^7.28.5"
"@babel/helper-validator-identifier": "^7.27.1"
},
"engines": {
"node": ">=6.9.0"
@@ -2445,6 +2443,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
},
@@ -2467,6 +2466,7 @@
}
],
"license": "MIT",
"peer": true,
"engines": {
"node": "^14 || ^16 || >=18"
}
@@ -2512,9 +2512,9 @@
"license": "GPL-3.0-or-later"
},
"node_modules/@edx/browserslist-config": {
"version": "1.5.1",
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.5.1.tgz",
"integrity": "sha512-r2zinEBFUqmh3iLkAb1RYwKDA0sQXjkP8OSl8dkE3Y+DnJwFIb1Yr1diY34vSwSQO5bB15OeLplFqQkbbPNpbA==",
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@edx/browserslist-config/-/browserslist-config-1.5.0.tgz",
"integrity": "sha512-d2ggwi5j4DOBJOwhWZxBWQSDR0DhT4ke/1PbzRauICdFkuOyax+PsFjK8GUh443K2OaQpy9PGfiCzZ1Yg37AUA==",
"dev": true,
"license": "AGPL-3.0"
},
@@ -2537,16 +2537,16 @@
}
},
"node_modules/@edx/frontend-platform": {
"version": "8.5.5",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.5.tgz",
"integrity": "sha512-imExY37cxE7qzKYg3gaqcdfhc0rzpV1DEFmy6PPCJg4m+cycQNiXtAKl3nITkcQkzhV0JYh3qttEgq6d4a1QXw==",
"version": "8.5.1",
"resolved": "https://registry.npmjs.org/@edx/frontend-platform/-/frontend-platform-8.5.1.tgz",
"integrity": "sha512-8u3EdO0o7xX4vqorjOx3k2wbs2bu3DXlIA3bnD+Y56vSB5QYw6k5GzYqo9pPaTMGeq9TuLRvPLE/QFFlc3xvPg==",
"license": "AGPL-3.0",
"dependencies": {
"@cospired/i18n-iso-languages": "4.2.0",
"@formatjs/intl-pluralrules": "4.3.3",
"@formatjs/intl-relativetimeformat": "10.0.1",
"axios": "1.13.5",
"axios-cache-interceptor": "1.11.4",
"axios": "1.12.0",
"axios-cache-interceptor": "1.8.0",
"form-urlencoded": "4.1.4",
"glob": "7.2.3",
"history": "4.10.1",
@@ -2661,6 +2661,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz",
"integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2672,6 +2673,7 @@
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz",
"integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -2682,6 +2684,7 @@
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz",
"integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -3163,6 +3166,7 @@
"resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-svg-core/-/fontawesome-svg-core-6.7.2.tgz",
"integrity": "sha512-yxtOBWDrdi5DD5o1pmVdq3WMCvnobT0LU6R8RyyVXPvFRd2o79/0NCuQoCjNTeZz9EzA9xS3JxNWfv54RIHFEA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@fortawesome/fontawesome-common-types": "6.7.2"
},
@@ -3265,6 +3269,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3287,6 +3292,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3309,6 +3315,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3325,6 +3332,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3341,6 +3349,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3357,6 +3366,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3373,6 +3383,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3389,6 +3400,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3405,6 +3417,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3421,6 +3434,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3437,6 +3451,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3453,6 +3468,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3475,6 +3491,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3497,6 +3514,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3519,6 +3537,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3541,6 +3560,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3563,6 +3583,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3585,6 +3606,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0",
"optional": true,
"os": [
@@ -3607,6 +3629,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
"optional": true,
"dependencies": {
@@ -3626,6 +3649,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3645,6 +3669,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3664,6 +3689,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "Apache-2.0 AND LGPL-3.0-or-later",
"optional": true,
"os": [
@@ -3847,17 +3873,17 @@
}
},
"node_modules/@jest/console": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-30.3.0.tgz",
"integrity": "sha512-PAwCvFJ4696XP2qZj+LAn1BWjZaJ6RjG6c7/lkMaUJnkyMS34ucuIsfqYvfskVNvUI27R/u4P1HMYFnlVXG/Ww==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/console/-/console-30.2.0.tgz",
"integrity": "sha512-+O1ifRjkvYIkBqASKWgLxrpEhQAAE7hY77ALLUufSk5717KfOShg6IbqLmdsLMPdUiFvA2kTs0R7YZy+l0IzZQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"jest-message-util": "30.3.0",
"jest-util": "30.3.0",
"jest-message-util": "30.2.0",
"jest-util": "30.2.0",
"slash": "^3.0.0"
},
"engines": {
@@ -3868,7 +3894,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -3878,10 +3904,10 @@
}
},
"node_modules/@jest/console/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -3897,17 +3923,17 @@
}
},
"node_modules/@jest/console/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/console/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -3917,19 +3943,19 @@
}
},
"node_modules/@jest/console/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -3938,18 +3964,18 @@
}
},
"node_modules/@jest/console/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -3959,7 +3985,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -3969,10 +3995,10 @@
}
},
"node_modules/@jest/console/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -3987,52 +4013,53 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jest/console/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/core": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/core/-/core-30.3.0.tgz",
"integrity": "sha512-U5mVPsBxLSO6xYbf+tgkymLx+iAhvZX43/xI1+ej2ZOPnPdkdO1CzDmFKh2mZBn2s4XZixszHeQnzp1gm/DIxw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/core/-/core-30.2.0.tgz",
"integrity": "sha512-03W6IhuhjqTlpzh/ojut/pDB2LPRygyWX8ExpgHtQA8H/3K7+1vKmcINx5UzeOX1se6YEsBsOHQ1CRzf3fOwTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "30.3.0",
"@jest/console": "30.2.0",
"@jest/pattern": "30.0.1",
"@jest/reporters": "30.3.0",
"@jest/test-result": "30.3.0",
"@jest/transform": "30.3.0",
"@jest/types": "30.3.0",
"@jest/reporters": "30.2.0",
"@jest/test-result": "30.2.0",
"@jest/transform": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
"jest-changed-files": "30.3.0",
"jest-config": "30.3.0",
"jest-haste-map": "30.3.0",
"jest-message-util": "30.3.0",
"jest-changed-files": "30.2.0",
"jest-config": "30.2.0",
"jest-haste-map": "30.2.0",
"jest-message-util": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-resolve": "30.3.0",
"jest-resolve-dependencies": "30.3.0",
"jest-runner": "30.3.0",
"jest-runtime": "30.3.0",
"jest-snapshot": "30.3.0",
"jest-util": "30.3.0",
"jest-validate": "30.3.0",
"jest-watcher": "30.3.0",
"pretty-format": "30.3.0",
"jest-resolve": "30.2.0",
"jest-resolve-dependencies": "30.2.0",
"jest-runner": "30.2.0",
"jest-runtime": "30.2.0",
"jest-snapshot": "30.2.0",
"jest-util": "30.2.0",
"jest-validate": "30.2.0",
"jest-watcher": "30.2.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0"
},
"engines": {
@@ -4048,21 +4075,21 @@
}
},
"node_modules/@jest/core/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -4082,7 +4109,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4092,23 +4119,24 @@
}
},
"node_modules/@jest/core/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -4118,10 +4146,10 @@
}
},
"node_modules/@jest/core/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -4137,17 +4165,17 @@
}
},
"node_modules/@jest/core/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/core/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -4160,7 +4188,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -4180,7 +4208,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -4194,10 +4222,10 @@
}
},
"node_modules/@jest/core/node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -4207,21 +4235,21 @@
}
},
"node_modules/@jest/core/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -4232,19 +4260,19 @@
}
},
"node_modules/@jest/core/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -4256,25 +4284,25 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/core/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4284,7 +4312,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4294,10 +4322,10 @@
}
},
"node_modules/@jest/core/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -4312,14 +4340,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jest/core/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -4332,7 +4360,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4342,7 +4370,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -4353,26 +4381,26 @@
}
},
"node_modules/@jest/diff-sequences": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.3.0.tgz",
"integrity": "sha512-cG51MVnLq1ecVUaQ3fr6YuuAOitHK1S4WUJHnsPFE/quQr33ADUx1FfrTCpMCRxvy0Yr9BThKpDjSlcTi91tMA==",
"devOptional": true,
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz",
"integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==",
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/environment": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz",
"integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz",
"integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/fake-timers": "30.3.0",
"@jest/types": "30.3.0",
"@jest/fake-timers": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"jest-mock": "30.3.0"
"jest-mock": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4382,7 +4410,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4392,10 +4420,10 @@
}
},
"node_modules/@jest/environment/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -4411,21 +4439,21 @@
}
},
"node_modules/@jest/environment/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/expect": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.3.0.tgz",
"integrity": "sha512-76Nlh4xJxk2D/9URCn3wFi98d2hb19uWE1idLsTt2ywhvdOldbw3S570hBgn25P4ICUZ/cBjybrBex2g17IDbg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.2.0.tgz",
"integrity": "sha512-V9yxQK5erfzx99Sf+7LbhBwNWEZ9eZay8qQ9+JSC0TrMR1pMDHLMY+BnVPacWU6Jamrh252/IKo4F1Xn/zfiqA==",
"dev": true,
"license": "MIT",
"dependencies": {
"expect": "30.3.0",
"jest-snapshot": "30.3.0"
"expect": "30.2.0",
"jest-snapshot": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4445,10 +4473,10 @@
}
},
"node_modules/@jest/expect/node_modules/@jest/expect-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz",
"integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz",
"integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0"
@@ -4461,7 +4489,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4471,10 +4499,10 @@
}
},
"node_modules/@jest/expect/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -4490,17 +4518,17 @@
}
},
"node_modules/@jest/expect/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/expect/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -4510,69 +4538,69 @@
}
},
"node_modules/@jest/expect/node_modules/expect": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz",
"integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz",
"integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/expect-utils": "30.3.0",
"@jest/expect-utils": "30.2.0",
"@jest/get-type": "30.1.0",
"jest-matcher-utils": "30.3.0",
"jest-message-util": "30.3.0",
"jest-mock": "30.3.0",
"jest-util": "30.3.0"
"jest-matcher-utils": "30.2.0",
"jest-message-util": "30.2.0",
"jest-mock": "30.2.0",
"jest-util": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect/node_modules/jest-diff": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
"integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
"integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/diff-sequences": "30.3.0",
"@jest/diff-sequences": "30.0.1",
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"pretty-format": "30.3.0"
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect/node_modules/jest-matcher-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz",
"integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz",
"integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"jest-diff": "30.3.0",
"pretty-format": "30.3.0"
"jest-diff": "30.2.0",
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/expect/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -4581,18 +4609,18 @@
}
},
"node_modules/@jest/expect/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4602,7 +4630,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4612,10 +4640,10 @@
}
},
"node_modules/@jest/expect/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -4630,32 +4658,32 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jest/expect/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/@jest/fake-timers": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz",
"integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz",
"integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@sinonjs/fake-timers": "^15.0.0",
"@jest/types": "30.2.0",
"@sinonjs/fake-timers": "^13.0.0",
"@types/node": "*",
"jest-message-util": "30.3.0",
"jest-mock": "30.3.0",
"jest-util": "30.3.0"
"jest-message-util": "30.2.0",
"jest-mock": "30.2.0",
"jest-util": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4665,7 +4693,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4675,10 +4703,10 @@
}
},
"node_modules/@jest/fake-timers/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -4694,17 +4722,17 @@
}
},
"node_modules/@jest/fake-timers/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/fake-timers/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -4714,19 +4742,19 @@
}
},
"node_modules/@jest/fake-timers/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -4735,18 +4763,18 @@
}
},
"node_modules/@jest/fake-timers/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4756,7 +4784,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -4766,10 +4794,10 @@
}
},
"node_modules/@jest/fake-timers/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -4784,14 +4812,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jest/fake-timers/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -4801,23 +4829,23 @@
"version": "30.1.0",
"resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz",
"integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/globals": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.3.0.tgz",
"integrity": "sha512-+owLCBBdfpgL3HU+BD5etr1SvbXpSitJK0is1kiYjJxAAJggYMRQz5hSdd5pq1sSggfxPbw2ld71pt4x5wwViA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.2.0.tgz",
"integrity": "sha512-b63wmnKPaK+6ZZfpYhz9K61oybvbI1aMcIs80++JI1O1rR1vaxHUCNqo3ITu6NU0d4V34yZFoHMn/uoKr/Rwfw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "30.3.0",
"@jest/expect": "30.3.0",
"@jest/types": "30.3.0",
"jest-mock": "30.3.0"
"@jest/environment": "30.2.0",
"@jest/expect": "30.2.0",
"@jest/types": "30.2.0",
"jest-mock": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -4827,7 +4855,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4837,10 +4865,10 @@
}
},
"node_modules/@jest/globals/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -4856,17 +4884,17 @@
}
},
"node_modules/@jest/globals/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/pattern": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz",
"integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@@ -4880,39 +4908,39 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/reporters": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.3.0.tgz",
"integrity": "sha512-a09z89S+PkQnL055bVj8+pe2Caed2PBOaczHcXCykW5ngxX9EWx/1uAwncxc/HiU0oZqfwseMjyhxgRjS49qPw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.2.0.tgz",
"integrity": "sha512-DRyW6baWPqKMa9CzeiBjHwjd8XeAyco2Vt8XbcLFjiwCOEKOvy82GJ8QQnJE9ofsxCMPjH4MfH8fCWIHHDKpAQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@bcoe/v8-coverage": "^0.2.3",
"@jest/console": "30.3.0",
"@jest/test-result": "30.3.0",
"@jest/transform": "30.3.0",
"@jest/types": "30.3.0",
"@jest/console": "30.2.0",
"@jest/test-result": "30.2.0",
"@jest/transform": "30.2.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"@types/node": "*",
"chalk": "^4.1.2",
"collect-v8-coverage": "^1.0.2",
"exit-x": "^0.2.2",
"glob": "^10.5.0",
"glob": "^10.3.10",
"graceful-fs": "^4.2.11",
"istanbul-lib-coverage": "^3.0.0",
"istanbul-lib-instrument": "^6.0.0",
"istanbul-lib-report": "^3.0.0",
"istanbul-lib-source-maps": "^5.0.0",
"istanbul-reports": "^3.1.3",
"jest-message-util": "30.3.0",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"jest-message-util": "30.2.0",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"slash": "^3.0.0",
"string-length": "^4.0.2",
"v8-to-istanbul": "^9.0.1"
@@ -4930,21 +4958,21 @@
}
},
"node_modules/@jest/reporters/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -4964,7 +4992,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -4974,23 +5002,24 @@
}
},
"node_modules/@jest/reporters/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -5000,10 +5029,10 @@
}
},
"node_modules/@jest/reporters/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -5019,17 +5048,17 @@
}
},
"node_modules/@jest/reporters/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/reporters/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -5042,7 +5071,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -5062,18 +5091,17 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/@jest/reporters/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"devOptional": true,
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -5094,7 +5122,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -5108,10 +5136,10 @@
}
},
"node_modules/@jest/reporters/node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -5121,21 +5149,21 @@
}
},
"node_modules/@jest/reporters/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -5146,19 +5174,19 @@
}
},
"node_modules/@jest/reporters/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -5170,38 +5198,38 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/reporters/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/reporters/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"devOptional": true,
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -5214,7 +5242,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -5224,10 +5252,10 @@
}
},
"node_modules/@jest/reporters/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -5242,14 +5270,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/@jest/reporters/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -5262,7 +5290,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5272,7 +5300,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -5296,13 +5324,13 @@
}
},
"node_modules/@jest/snapshot-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.3.0.tgz",
"integrity": "sha512-ORbRN9sf5PP82v3FXNSwmO1OTDR2vzR2YTaR+E3VkSBZ8zadQE6IqYdYEeFH1NIkeB2HIGdF02dapb6K0Mj05g==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.2.0.tgz",
"integrity": "sha512-0aVxM3RH6DaiLcjj/b0KrIBZhSX1373Xci4l3cW5xiUWPctZ59zQ7jj4rqcJQ/Z8JuN/4wX3FpJSa3RssVvCug==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"natural-compare": "^1.4.0"
@@ -5315,7 +5343,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -5325,10 +5353,10 @@
}
},
"node_modules/@jest/snapshot-utils/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -5344,17 +5372,17 @@
}
},
"node_modules/@jest/snapshot-utils/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/source-map": {
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz",
"integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
@@ -5366,14 +5394,14 @@
}
},
"node_modules/@jest/test-result": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.3.0.tgz",
"integrity": "sha512-e/52nJGuD74AKTSe0P4y5wFRlaXP0qmrS17rqOMHeSwm278VyNyXE3gFO/4DTGF9w+65ra3lo3VKj0LBrzmgdQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.2.0.tgz",
"integrity": "sha512-RF+Z+0CCHkARz5HT9mcQCBulb1wgCP3FBvl9VFokMX27acKphwyQsNuWH3c+ojd1LeWBLoTYoxF0zm6S/66mjg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "30.3.0",
"@jest/types": "30.3.0",
"@jest/console": "30.2.0",
"@jest/types": "30.2.0",
"@types/istanbul-lib-coverage": "^2.0.6",
"collect-v8-coverage": "^1.0.2"
},
@@ -5385,7 +5413,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -5395,10 +5423,10 @@
}
},
"node_modules/@jest/test-result/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -5414,22 +5442,22 @@
}
},
"node_modules/@jest/test-result/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/test-sequencer": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.3.0.tgz",
"integrity": "sha512-dgbWy9b8QDlQeRZcv7LNF+/jFiiYHTKho1xirauZ7kVwY7avjFF6uTT0RqlgudB5OuIPagFdVtfFMosjVbk1eA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.2.0.tgz",
"integrity": "sha512-wXKgU/lk8fKXMu/l5Hog1R61bL4q5GCdT6OJvdAFz1P+QrpoFuLU68eoKuVc4RbrTtNnTL5FByhWdLgOPSph+Q==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/test-result": "30.3.0",
"@jest/test-result": "30.2.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"slash": "^3.0.0"
},
"engines": {
@@ -5440,7 +5468,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -5450,10 +5478,10 @@
}
},
"node_modules/@jest/test-sequencer/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -5469,28 +5497,28 @@
}
},
"node_modules/@jest/test-sequencer/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/@jest/test-sequencer/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -5504,25 +5532,25 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/@jest/test-sequencer/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -5532,7 +5560,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -5545,7 +5573,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -5791,6 +5819,7 @@
"version": "0.2.12",
"resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz",
"integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -5878,6 +5907,7 @@
"version": "2.1.8-no-fsevents.3",
"resolved": "https://registry.npmjs.org/@nicolo-ribaudo/chokidar-2/-/chokidar-2-2.1.8-no-fsevents.3.tgz",
"integrity": "sha512-s88O1aVtXftvp5bCPB7WnmXc5IwOZZ7YPuwNPt+GtOOXpPvad1LfbmjYv+qII7zP6RU2QGnqve27dnLycEnyEQ==",
"dev": true,
"license": "MIT",
"optional": true
},
@@ -5948,6 +5978,7 @@
"integrity": "sha512-Iu4/GPq90Xr/MSWnonn2qX8VDhI89HN7KOYBZ0/sxmAQgvXXNc7OYNC7kumvzbYzKueJQTyZoUYS7UjKB/n1WA==",
"devOptional": true,
"license": "AGPL-3.0",
"peer": true,
"dependencies": {
"@babel/cli": "7.24.8",
"@babel/core": "7.24.9",
@@ -6466,6 +6497,7 @@
"integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@jest/core": "^29.7.0",
"@jest/types": "^29.6.3",
@@ -7028,88 +7060,12 @@
"node": ">=10"
}
},
"node_modules/@openedx/frontend-plugin-framework": {
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/@openedx/frontend-plugin-framework/-/frontend-plugin-framework-1.8.0.tgz",
"integrity": "sha512-QLH4KTkk+dT8SwrBpEaWS0T0htYhaIS8i36ngXraUEe+Iabh6NVN0I84bQa5vaAhz5v101GNmdUXBanf5waPgQ==",
"license": "AGPL-3.0",
"dependencies": {
"@edx/brand": "npm:@openedx/brand-openedx@^1.2.2",
"classnames": "^2.3.2",
"core-js": "3.37.1",
"react-redux": "8.1.1",
"redux": "4.2.1"
},
"peerDependencies": {
"@edx/frontend-platform": "^7.0.0 || ^8.0.0",
"@openedx/paragon": "^21.0.0 || ^22.0.0 || ^23.0.0",
"prop-types": "^15.8.0",
"react": "^17.0.0 || ^18.0.0",
"react-dom": "^17.0.0 || ^18.0.0",
"react-error-boundary": "^4.0.11"
}
},
"node_modules/@openedx/frontend-plugin-framework/node_modules/core-js": {
"version": "3.37.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.1.tgz",
"integrity": "sha512-Xn6qmxrQZyB0FFY8E3bgRXei3lWDJHhvI+u0q9TKIYM49G8pAr0FgnnrFRAmsbptZL1yxRADVXn+x5AGsbBfyw==",
"hasInstallScript": true,
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/core-js"
}
},
"node_modules/@openedx/frontend-plugin-framework/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/@openedx/frontend-plugin-framework/node_modules/react-redux": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-8.1.1.tgz",
"integrity": "sha512-5W0QaKtEhj+3bC0Nj0NkqkhIv8gLADH/2kYFMTHxCVqQILiWzLv6MaLuV5wJU3BQEdHKzTfcvPN0WMS6SC1oyA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.12.1",
"@types/hoist-non-react-statics": "^3.3.1",
"@types/use-sync-external-store": "^0.0.3",
"hoist-non-react-statics": "^3.3.2",
"react-is": "^18.0.0",
"use-sync-external-store": "^1.0.0"
},
"peerDependencies": {
"@types/react": "^16.8 || ^17.0 || ^18.0",
"@types/react-dom": "^16.8 || ^17.0 || ^18.0",
"react": "^16.8 || ^17.0 || ^18.0",
"react-dom": "^16.8 || ^17.0 || ^18.0",
"react-native": ">=0.59",
"redux": "^4 || ^5.0.0-beta.0"
},
"peerDependenciesMeta": {
"@types/react": {
"optional": true
},
"@types/react-dom": {
"optional": true
},
"react-dom": {
"optional": true
},
"react-native": {
"optional": true
},
"redux": {
"optional": true
}
}
},
"node_modules/@openedx/paragon": {
"version": "23.19.1",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.19.1.tgz",
"integrity": "sha512-c/cWnvZsGS7xyq0tJpssmv2oyfYG6Fuawy6EzWy8CYiQ4oD67EVuSwBInCfSJoNZhvvkUE+4B/YhDIRGUVDz5w==",
"version": "23.14.9",
"resolved": "https://registry.npmjs.org/@openedx/paragon/-/paragon-23.14.9.tgz",
"integrity": "sha512-IwM6UZJE6lKmGCyGV52oO+40m0y2kFAoOdzyFdw3ud+Wc0Ro3bcflPnuNuq2Wq5a3ceJ9RLu9g+uKx0Yv9yvrw==",
"license": "Apache-2.0",
"peer": true,
"workspaces": [
"example",
"component-generator",
@@ -7729,7 +7685,7 @@
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz",
"integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^12.20.0 || ^14.18.0 || >=16.0.0"
@@ -7799,15 +7755,85 @@
"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"
}
},
"node_modules/@redux-devtools/extension": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/@redux-devtools/extension/-/extension-3.3.0.tgz",
"integrity": "sha512-X34S/rC8S/M1BIrkYD1mJ5f8vlH0BDqxXrs96cvxSBo4FhMdbhU+GUGsmNYov1xjSyLMHgo8NYrUG8bNX7525g==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.23.2",
"immutable": "^4.3.4"
},
"peerDependencies": {
"redux": "^3.1.0 || ^4.0.0 || ^5.0.0"
}
},
"node_modules/@redux-saga/core": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@redux-saga/core/-/core-1.3.0.tgz",
"integrity": "sha512-L+i+qIGuyWn7CIg7k1MteHGfttKPmxwZR5E7OsGikCL2LzYA0RERlaUY00Y3P3ZV2EYgrsYlBrGs6cJP5OKKqA==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "^7.6.3",
"@redux-saga/deferred": "^1.2.1",
"@redux-saga/delay-p": "^1.2.1",
"@redux-saga/is": "^1.1.3",
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1",
"typescript-tuple": "^2.2.1"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/redux-saga"
}
},
"node_modules/@redux-saga/deferred": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/deferred/-/deferred-1.2.1.tgz",
"integrity": "sha512-cmin3IuuzMdfQjA0lG4B+jX+9HdTgHZZ+6u3jRAOwGUxy77GSlTi4Qp2d6PM1PUoTmQUR5aijlA39scWWPF31g==",
"license": "MIT"
},
"node_modules/@redux-saga/delay-p": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/delay-p/-/delay-p-1.2.1.tgz",
"integrity": "sha512-MdiDxZdvb1m+Y0s4/hgdcAXntpUytr9g0hpcOO1XFVyyzkrDu3SKPgBFOtHn7lhu7n24ZKIAT1qtKyQjHqRd+w==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.1.3"
}
},
"node_modules/@redux-saga/is": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/is/-/is-1.1.3.tgz",
"integrity": "sha512-naXrkETG1jLRfVfhOx/ZdLj0EyAzHYbgJWkXbB3qFliPcHKiWbv/ULQryOAEKyjrhiclmr6AMdgsXFyx7/yE6Q==",
"license": "MIT",
"dependencies": {
"@redux-saga/symbols": "^1.1.3",
"@redux-saga/types": "^1.2.1"
}
},
"node_modules/@redux-saga/symbols": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/@redux-saga/symbols/-/symbols-1.1.3.tgz",
"integrity": "sha512-hCx6ZvU4QAEUojETnX8EVg4ubNLBFl1Lps4j2tX7o45x/2qg37m3c6v+kSp8xjDJY+2tJw4QB3j8o8dsl1FDXg==",
"license": "MIT"
},
"node_modules/@redux-saga/types": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/@redux-saga/types/-/types-1.2.1.tgz",
"integrity": "sha512-1dgmkh+3so0+LlBWRhGA33ua4MYr7tUOj+a9Si28vUi0IUFNbff1T3sgpeDJI/LaC75bBYnQ0A3wXjn0OrRNBA==",
"license": "MIT"
},
"node_modules/@remix-run/router": {
"version": "1.23.2",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.2.tgz",
"integrity": "sha512-Ic6m2U/rMjTkhERIa/0ZtXJP17QUi2CbWE7cqx4J58M8aA3QTfW+2UlQ4psvTX9IO1RfNVhK3pcpdjej7L+t2w==",
"version": "1.23.0",
"resolved": "https://registry.npmjs.org/@remix-run/router/-/router-1.23.0.tgz",
"integrity": "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==",
"license": "MIT",
"engines": {
"node": ">=14.0.0"
@@ -7859,10 +7885,10 @@
}
},
"node_modules/@sinonjs/fake-timers": {
"version": "15.1.1",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz",
"integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==",
"devOptional": true,
"version": "13.0.5",
"resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz",
"integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@sinonjs/commons": "^3.0.1"
@@ -8037,6 +8063,7 @@
"integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/core": "^7.21.3",
"@svgr/babel-preset": "8.1.0",
@@ -8139,32 +8166,6 @@
"url": "https://github.com/sponsors/gregberge"
}
},
"node_modules/@tanstack/query-core": {
"version": "5.90.20",
"resolved": "https://registry.npmjs.org/@tanstack/query-core/-/query-core-5.90.20.tgz",
"integrity": "sha512-OMD2HLpNouXEfZJWcKeVKUgQ5n+n3A2JFmBaScpNDUqSrQSjiveC7dKMe53uJUg1nDG16ttFPz2xfilz6i2uVg==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/react-query": {
"version": "5.90.21",
"resolved": "https://registry.npmjs.org/@tanstack/react-query/-/react-query-5.90.21.tgz",
"integrity": "sha512-0Lu6y5t+tvlTJMTO7oh5NSpJfpg/5D41LlThfepTixPYkJ0sE2Jj0m0f6yYqujBwIXlId87e234+MxG3D3g7kg==",
"license": "MIT",
"dependencies": {
"@tanstack/query-core": "5.90.20"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^18 || ^19"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",
@@ -8185,33 +8186,6 @@
"node": ">=18"
}
},
"node_modules/@testing-library/jest-dom": {
"version": "6.9.1",
"resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz",
"integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@adobe/css-tools": "^4.4.0",
"aria-query": "^5.0.0",
"css.escape": "^1.5.1",
"dom-accessibility-api": "^0.6.3",
"picocolors": "^1.1.1",
"redent": "^3.0.0"
},
"engines": {
"node": ">=14",
"npm": ">=6",
"yarn": ">=1"
}
},
"node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": {
"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",
@@ -8289,6 +8263,7 @@
"version": "0.10.1",
"resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz",
"integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==",
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -8299,8 +8274,7 @@
"version": "5.0.4",
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -8662,6 +8636,7 @@
"integrity": "sha512-gUuVEAK4/u6F9wRLznPUU4WGUacSEBDPoC2TrBkw3GAnOLHBL45QdfHOXp1kJ4ypBGLxTOB+t7NJLpKoC3gznQ==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"undici-types": "~7.11.0"
}
@@ -8711,12 +8686,12 @@
"license": "MIT"
},
"node_modules/@types/react": {
"version": "18.3.26",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.26.tgz",
"integrity": "sha512-RFA/bURkcKzx/X9oumPG9Vp3D3JUgus/d0b67KB0t5S/raciymilkOa66olh78MUI92QLbEJevO7rvqU/kjwKA==",
"version": "19.1.13",
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz",
"integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
@@ -8725,7 +8700,6 @@
"resolved": "https://registry.npmjs.org/@types/react-redux/-/react-redux-7.1.34.tgz",
"integrity": "sha512-GdFaVjEbYv4Fthm2ZLvj1VSCedV7TqE5y1kNwnjSdBOTXuRSgowux6J8TAct15T3CKBr63UMk+2CO7ilRhyrAQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@types/hoist-non-react-statics": "^3.3.0",
"@types/react": "*",
@@ -8813,12 +8787,6 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/@types/use-sync-external-store": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/@types/use-sync-external-store/-/use-sync-external-store-0.0.3.tgz",
"integrity": "sha512-EwmlvuaxPNej9+T4v5AuBPJa2x2UOJVdjCtDHgcDqitUeOtjnJKJ+apYjVcAoBEMjKW1VVFGZLUb5+qqa09XFA==",
"license": "MIT"
},
"node_modules/@types/warning": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz",
@@ -8858,6 +8826,7 @@
"integrity": "sha512-TiZzBSJja/LbhNPvk6yc0JrX9XqhQ0hdh6M2svYfsHGejaKFIAGd9MQ+ERIMzLGlN/kZoYIgdxFV0PuljTKXag==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/regexpp": "^4.4.0",
"@typescript-eslint/scope-manager": "5.62.0",
@@ -8906,6 +8875,7 @@
"integrity": "sha512-VlJEV0fOQ7BExOsHYAGrgbEiZoi8D+Bl2+f6V2RrXerRSylnp+ZBHmPvaIa8cz0Ajx7WO7Z5RqfgYg7ED1nRhA==",
"devOptional": true,
"license": "BSD-2-Clause",
"peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "5.62.0",
"@typescript-eslint/types": "5.62.0",
@@ -9104,7 +9074,7 @@
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
"integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==",
"devOptional": true,
"dev": true,
"license": "ISC"
},
"node_modules/@unrs/resolver-binding-android-arm-eabi": {
@@ -9114,6 +9084,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9127,6 +9098,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9140,6 +9112,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9153,6 +9126,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9166,6 +9140,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9179,6 +9154,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9192,6 +9168,7 @@
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9205,6 +9182,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9218,6 +9196,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9231,6 +9210,7 @@
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9244,6 +9224,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9257,6 +9238,7 @@
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9270,6 +9252,7 @@
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9283,6 +9266,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9296,6 +9280,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9309,6 +9294,7 @@
"cpu": [
"wasm32"
],
"dev": true,
"license": "MIT",
"optional": true,
"dependencies": {
@@ -9325,6 +9311,7 @@
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9338,6 +9325,7 @@
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9351,6 +9339,7 @@
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
@@ -9624,6 +9613,7 @@
"integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -9721,6 +9711,7 @@
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
@@ -9785,32 +9776,33 @@
}
},
"node_modules/algoliasearch": {
"version": "4.27.0",
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.27.0.tgz",
"integrity": "sha512-C88C5grLa5VOCp9eYZJt+q99ik7yNdm92l7Q9+4XK0Md8kL05Lg8l2v9ZVX0uMW3mH9pAFxMMXlLOvqNumA4lw==",
"version": "4.25.2",
"resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-4.25.2.tgz",
"integrity": "sha512-lYx98L6kb1VvXypbPI7Z54C4BJB2VT5QvOYthvPq6/POufZj+YdyeZSKjoLBKHJgGmYWQTHOKtcCTdKf98WOCA==",
"license": "MIT",
"peer": true,
"dependencies": {
"@algolia/cache-browser-local-storage": "4.27.0",
"@algolia/cache-common": "4.27.0",
"@algolia/cache-in-memory": "4.27.0",
"@algolia/client-account": "4.27.0",
"@algolia/client-analytics": "4.27.0",
"@algolia/client-common": "4.27.0",
"@algolia/client-personalization": "4.27.0",
"@algolia/client-search": "4.27.0",
"@algolia/logger-common": "4.27.0",
"@algolia/logger-console": "4.27.0",
"@algolia/recommend": "4.27.0",
"@algolia/requester-browser-xhr": "4.27.0",
"@algolia/requester-common": "4.27.0",
"@algolia/requester-node-http": "4.27.0",
"@algolia/transporter": "4.27.0"
"@algolia/cache-browser-local-storage": "4.25.2",
"@algolia/cache-common": "4.25.2",
"@algolia/cache-in-memory": "4.25.2",
"@algolia/client-account": "4.25.2",
"@algolia/client-analytics": "4.25.2",
"@algolia/client-common": "4.25.2",
"@algolia/client-personalization": "4.25.2",
"@algolia/client-search": "4.25.2",
"@algolia/logger-common": "4.25.2",
"@algolia/logger-console": "4.25.2",
"@algolia/recommend": "4.25.2",
"@algolia/requester-browser-xhr": "4.25.2",
"@algolia/requester-common": "4.25.2",
"@algolia/requester-node-http": "4.25.2",
"@algolia/transporter": "4.25.2"
}
},
"node_modules/algoliasearch-helper": {
"version": "3.28.0",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.28.0.tgz",
"integrity": "sha512-GBN0xsxGggaCPElZq24QzMdfphrjIiV2xA+hRXE4/UMpN3nsF2WrM8q+x80OGvGpJWtB7F+4Hq5eSfWwuejXrg==",
"version": "3.26.0",
"resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz",
"integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==",
"license": "MIT",
"dependencies": {
"@algolia/events": "^4.0.1"
@@ -10260,27 +10252,26 @@
}
},
"node_modules/axios": {
"version": "1.13.5",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.13.5.tgz",
"integrity": "sha512-cz4ur7Vb0xS4/KUN0tPWe44eqxrIu31me+fbang3ijiNscE129POzipJJA6zniq2C/Z6sJCjMimjS8Lc/GAs8Q==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.12.0.tgz",
"integrity": "sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==",
"license": "MIT",
"peer": true,
"dependencies": {
"follow-redirects": "^1.15.11",
"form-data": "^4.0.5",
"follow-redirects": "^1.15.6",
"form-data": "^4.0.4",
"proxy-from-env": "^1.1.0"
}
},
"node_modules/axios-cache-interceptor": {
"version": "1.11.4",
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.11.4.tgz",
"integrity": "sha512-xZ4OZUxdpcFUpZjrqfYlGK0VglpPRKKSoE3vMHrstxolixQNs/MrbMezOAO5uS454hIEcWpnk75RZK26WkPW/g==",
"version": "1.8.0",
"resolved": "https://registry.npmjs.org/axios-cache-interceptor/-/axios-cache-interceptor-1.8.0.tgz",
"integrity": "sha512-cTNnPGJyQkxnWp0EWvE3NRvgURU5cWw/Qx3dIhXyHSM4Ip0c7EEe0I3an0Jwa549m1CAOg57ibj27YRNLmQCcg==",
"license": "MIT",
"dependencies": {
"cache-parser": "^1.2.6",
"fast-defer": "^1.1.9",
"http-vary": "^1.0.3",
"object-code": "^2.0.0",
"try": "^1.0.3"
"cache-parser": "1.2.5",
"fast-defer": "1.1.8",
"object-code": "1.3.3"
},
"engines": {
"node": ">=12"
@@ -10787,6 +10778,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.8.2",
"caniuse-lite": "^1.0.30001741",
@@ -10866,9 +10858,9 @@
}
},
"node_modules/cache-parser": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.6.tgz",
"integrity": "sha512-SjjnKlWgrhDrAWKUxAvmZLRGDa6JExMfjSu59/pvpNoI6mEHYSLcLKUw2RtECEOINvf6dxJo35fY+T/scA0SUA==",
"version": "1.2.5",
"resolved": "https://registry.npmjs.org/cache-parser/-/cache-parser-1.2.5.tgz",
"integrity": "sha512-Md/4VhAHByQ9frQ15WD6LrMNiVw9AEl/J7vWIXw+sxT6fSOpbtt6LHTp76vy8+bOESPBO94117Hm2bIjlI7XjA==",
"license": "MIT"
},
"node_modules/call-bind": {
@@ -11104,7 +11096,7 @@
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz",
"integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==",
"devOptional": true,
"dev": true,
"funding": [
{
"type": "github",
@@ -11117,10 +11109,10 @@
}
},
"node_modules/cjs-module-lexer": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.0.tgz",
"integrity": "sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==",
"devOptional": true,
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz",
"integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==",
"dev": true,
"license": "MIT"
},
"node_modules/classnames": {
@@ -12360,13 +12352,6 @@
"url": "https://github.com/sponsors/fb55"
}
},
"node_modules/css.escape": {
"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": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@@ -12671,6 +12656,12 @@
}
}
},
"node_modules/deep-diff": {
"version": "0.3.8",
"resolved": "https://registry.npmjs.org/deep-diff/-/deep-diff-0.3.8.tgz",
"integrity": "sha512-yVn6RZmHiGnxRKR9sJb3iVV2XTF1Ghh2DiWRZ3dMnGc43yUdWWF/kX6lQyk3+P84iprfWKU/8zFTrlkvtFm1ug==",
"license": "MIT"
},
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -12995,8 +12986,7 @@
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/dom-converter": {
"version": "0.2.0",
@@ -13552,6 +13542,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"optional": true,
"engines": {
@@ -13565,6 +13556,7 @@
"deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.4.0",
@@ -13622,6 +13614,7 @@
"integrity": "sha512-T75QYQVQX57jiNgpF9r1KegMICE94VYwoFQyMGhrvc+lB8YF2E/M/PYDaQe1AJcWaEgqLE+ErXV1Og/+6Vyzew==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0",
"object.assign": "^4.1.2",
@@ -13664,6 +13657,7 @@
"integrity": "sha512-GPxI5URre6dDpJ0CtcthSZVBAfI+Uw7un5OYNVxP2EYi3H81Jw701yFP7AU+/vCE7xBtFmjge7kfhhk4+RAiig==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"eslint-config-airbnb-base": "^15.0.0"
},
@@ -14066,6 +14060,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -14123,6 +14118,7 @@
"integrity": "sha512-63Bog4iIethyo8smBklORknVjB0T2dwB8Mr/hIC+fBS0uyHdYYpzM/Ed+YC8VxTjlXHEWFOdmgwcDn1U2L9VCA==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.20.7",
"aria-query": "^5.1.3",
@@ -14161,6 +14157,7 @@
"integrity": "sha512-73QQMKALArI8/7xGLNI/3LylrEYrlKZSb5C9+q3OtOewTnMQi5cT+aE9E41sLCmli3I9PGGmD1yiZydyo4FEPw==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"array-includes": "^3.1.6",
"array.prototype.flatmap": "^1.3.1",
@@ -14192,6 +14189,7 @@
"integrity": "sha512-Ck77j8hF7l9N4S/rzSLOWEKpn994YH6iwUK8fr9mXIaQvGpQYmOnQLbiue1u5kI5T1y+gdgqosnEAO9NCz0DBg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -14529,7 +14527,7 @@
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz",
"integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.8.0"
@@ -14630,9 +14628,9 @@
"license": "MIT"
},
"node_modules/fast-defer": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.9.tgz",
"integrity": "sha512-JP7Xm9HuePSeTT1DI78NeE9eAQvgNb9qNP2jlyQrcx4jiWM189omV6oyd0xaUPWHPlKmvDzz6H1FfPWIDU+xfg==",
"version": "1.1.8",
"resolved": "https://registry.npmjs.org/fast-defer/-/fast-defer-1.1.8.tgz",
"integrity": "sha512-lEJeOH5VL5R09j6AA0D4Uvq7AgsHw0dAImQQ+F3iSyHZuAxyQfWobsagGpTcOPvJr3urmKRHrs+Gs9hV+/Qm/Q==",
"license": "MIT"
},
"node_modules/fast-glob": {
@@ -15162,9 +15160,9 @@
}
},
"node_modules/form-data": {
"version": "4.0.5",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz",
"integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==",
"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",
@@ -15274,6 +15272,7 @@
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
@@ -15658,7 +15657,7 @@
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
@@ -15680,7 +15679,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@@ -16105,12 +16104,6 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/http-vary": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http-vary/-/http-vary-1.0.3.tgz",
"integrity": "sha512-sx7Y8YTqF3o0mFJJvF66n8dbaE8v3liV1RgCz46XP5xK7dnzyZHvwMWRA115q5kjbCPBV65/nOMlgW54WLyiag==",
"license": "MIT"
},
"node_modules/https-proxy-agent": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz",
@@ -16295,6 +16288,12 @@
"url": "https://opencollective.com/immer"
}
},
"node_modules/immutable": {
"version": "4.3.7",
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.7.tgz",
"integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==",
"license": "MIT"
},
"node_modules/import-fresh": {
"version": "3.3.1",
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz",
@@ -16365,16 +16364,6 @@
"node": ">=0.8.19"
}
},
"node_modules/indent-string": {
"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"
}
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
@@ -17302,7 +17291,7 @@
"version": "5.0.6",
"resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz",
"integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.23",
@@ -17361,16 +17350,16 @@
}
},
"node_modules/jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-30.3.0.tgz",
"integrity": "sha512-AkXIIFcaazymvey2i/+F94XRnM6TsVLZDhBMLsd1Sf/W0wzsvvpjeyUrCZD6HGG4SDYPgDJDBKeiJTBb10WzMg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest/-/jest-30.2.0.tgz",
"integrity": "sha512-F26gjC0yWN8uAA5m5Ss8ZQf5nDHWGlN/xWZIh8S5SRbsEKBovwZhxGd6LJlbZYxBgCYOtreSUyb8hpXyGC5O4A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/core": "30.3.0",
"@jest/types": "30.3.0",
"@jest/core": "30.2.0",
"@jest/types": "30.2.0",
"import-local": "^3.2.0",
"jest-cli": "30.3.0"
"jest-cli": "30.2.0"
},
"bin": {
"jest": "bin/jest.js"
@@ -17388,14 +17377,14 @@
}
},
"node_modules/jest-changed-files": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.3.0.tgz",
"integrity": "sha512-B/7Cny6cV5At6M25EWDgf9S617lHivamL8vl6KEpJqkStauzcG4e+WPfDgMMF+H4FVH4A2PLRyvgDJan4441QA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.2.0.tgz",
"integrity": "sha512-L8lR1ChrRnSdfeOvTrwZMlnWV8G/LLjQ0nG9MBclwWZidA2N5FviRki0Bvh20WRMOX31/JYvzdqTJrk5oBdydQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"execa": "^5.1.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"p-limit": "^3.1.0"
},
"engines": {
@@ -17406,7 +17395,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -17416,10 +17405,10 @@
}
},
"node_modules/jest-changed-files/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -17435,25 +17424,25 @@
}
},
"node_modules/jest-changed-files/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-changed-files/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -17463,7 +17452,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -17473,29 +17462,29 @@
}
},
"node_modules/jest-circus": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.3.0.tgz",
"integrity": "sha512-PyXq5szeSfR/4f1lYqCmmQjh0vqDkURUYi9N6whnHjlRz4IUQfMcXkGLeEoiJtxtyPqgUaUUfyQlApXWBSN1RA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.2.0.tgz",
"integrity": "sha512-Fh0096NC3ZkFx05EP2OXCxJAREVxj1BcW/i6EWqqymcgYKWjyyDpral3fMxVcHXg6oZM7iULer9wGRFvfpl+Tg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "30.3.0",
"@jest/expect": "30.3.0",
"@jest/test-result": "30.3.0",
"@jest/types": "30.3.0",
"@jest/environment": "30.2.0",
"@jest/expect": "30.2.0",
"@jest/test-result": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"co": "^4.6.0",
"dedent": "^1.6.0",
"is-generator-fn": "^2.1.0",
"jest-each": "30.3.0",
"jest-matcher-utils": "30.3.0",
"jest-message-util": "30.3.0",
"jest-runtime": "30.3.0",
"jest-snapshot": "30.3.0",
"jest-util": "30.3.0",
"jest-each": "30.2.0",
"jest-matcher-utils": "30.2.0",
"jest-message-util": "30.2.0",
"jest-runtime": "30.2.0",
"jest-snapshot": "30.2.0",
"jest-util": "30.2.0",
"p-limit": "^3.1.0",
"pretty-format": "30.3.0",
"pretty-format": "30.2.0",
"pure-rand": "^7.0.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
@@ -17508,7 +17497,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -17518,10 +17507,10 @@
}
},
"node_modules/jest-circus/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -17537,17 +17526,17 @@
}
},
"node_modules/jest-circus/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-circus/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -17557,51 +17546,51 @@
}
},
"node_modules/jest-circus/node_modules/jest-diff": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
"integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
"integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/diff-sequences": "30.3.0",
"@jest/diff-sequences": "30.0.1",
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"pretty-format": "30.3.0"
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-circus/node_modules/jest-matcher-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz",
"integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz",
"integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"jest-diff": "30.3.0",
"pretty-format": "30.3.0"
"jest-diff": "30.2.0",
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-circus/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -17610,18 +17599,18 @@
}
},
"node_modules/jest-circus/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -17631,7 +17620,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -17641,10 +17630,10 @@
}
},
"node_modules/jest-circus/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -17659,35 +17648,35 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-circus/node_modules/slash": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jest-cli": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.3.0.tgz",
"integrity": "sha512-l6Tqx+j1fDXJEW5bqYykDQQ7mQg+9mhWXtnj+tQZrTWYHyHoi6Be8HPumDSA+UiX2/2buEgjA58iJzdj146uCw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.2.0.tgz",
"integrity": "sha512-Os9ukIvADX/A9sLt6Zse3+nmHtHaE6hqOsjQtNiugFTbKRHYIYtZXNGNK9NChseXy7djFPjndX1tL0sCTlfpAA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/core": "30.3.0",
"@jest/test-result": "30.3.0",
"@jest/types": "30.3.0",
"@jest/core": "30.2.0",
"@jest/test-result": "30.2.0",
"@jest/types": "30.2.0",
"chalk": "^4.1.2",
"exit-x": "^0.2.2",
"import-local": "^3.2.0",
"jest-config": "30.3.0",
"jest-util": "30.3.0",
"jest-validate": "30.3.0",
"jest-config": "30.2.0",
"jest-util": "30.2.0",
"jest-validate": "30.2.0",
"yargs": "^17.7.2"
},
"bin": {
@@ -17709,7 +17698,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -17719,10 +17708,10 @@
}
},
"node_modules/jest-cli/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -17738,25 +17727,25 @@
}
},
"node_modules/jest-cli/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-cli/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -17766,7 +17755,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -17776,33 +17765,34 @@
}
},
"node_modules/jest-config": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.3.0.tgz",
"integrity": "sha512-WPMAkMAtNDY9P/oKObtsRG/6KTrhtgPJoBTmk20uDn4Uy6/3EJnnaZJre/FMT1KVRx8cve1r7/FlMIOfRVWL4w==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.2.0.tgz",
"integrity": "sha512-g4WkyzFQVWHtu6uqGmQR4CQxz/CH3yDSlhzXMWzNjDx843gYjReZnMRanjRCq5XZFuQrGDxgUaiYWE8BRfVckA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/get-type": "30.1.0",
"@jest/pattern": "30.0.1",
"@jest/test-sequencer": "30.3.0",
"@jest/types": "30.3.0",
"babel-jest": "30.3.0",
"@jest/test-sequencer": "30.2.0",
"@jest/types": "30.2.0",
"babel-jest": "30.2.0",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"deepmerge": "^4.3.1",
"glob": "^10.5.0",
"glob": "^10.3.10",
"graceful-fs": "^4.2.11",
"jest-circus": "30.3.0",
"jest-circus": "30.2.0",
"jest-docblock": "30.2.0",
"jest-environment-node": "30.3.0",
"jest-environment-node": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-resolve": "30.3.0",
"jest-runner": "30.3.0",
"jest-util": "30.3.0",
"jest-validate": "30.3.0",
"jest-resolve": "30.2.0",
"jest-runner": "30.2.0",
"jest-util": "30.2.0",
"jest-validate": "30.2.0",
"micromatch": "^4.0.8",
"parse-json": "^5.2.0",
"pretty-format": "30.3.0",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"strip-json-comments": "^3.1.1"
},
@@ -17827,21 +17817,22 @@
}
},
"node_modules/jest-config/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -17861,7 +17852,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -17871,23 +17862,24 @@
}
},
"node_modules/jest-config/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -17897,10 +17889,10 @@
}
},
"node_modules/jest-config/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -17916,17 +17908,17 @@
}
},
"node_modules/jest-config/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-config/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -17936,16 +17928,16 @@
}
},
"node_modules/jest-config/node_modules/babel-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.3.0.tgz",
"integrity": "sha512-gRpauEU2KRrCox5Z296aeVHR4jQ98BCnu0IO332D/xpHNOsIH/bgSRk9k6GbKIbBw8vFeN6ctuu6tV8WOyVfYQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.2.0.tgz",
"integrity": "sha512-0YiBEOxWqKkSQWL9nNGGEgndoeL0ZpWrbLMNL5u/Kaxrli3Eaxlt3ZtIDktEvXt4L/R9r3ODr2zKwGM/2BjxVw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/transform": "30.3.0",
"@jest/transform": "30.2.0",
"@types/babel__core": "^7.20.5",
"babel-plugin-istanbul": "^7.0.1",
"babel-preset-jest": "30.3.0",
"babel-preset-jest": "30.2.0",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"slash": "^3.0.0"
@@ -17961,7 +17953,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -17978,10 +17970,10 @@
}
},
"node_modules/jest-config/node_modules/babel-plugin-jest-hoist": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.3.0.tgz",
"integrity": "sha512-+TRkByhsws6sfPjVaitzadk1I0F5sPvOVUH5tyTSzhePpsGIVrdeunHSw/C36QeocS95OOk8lunc4rlu5Anwsg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.2.0.tgz",
"integrity": "sha512-ftzhzSGMUnOzcCXd6WHdBGMyuwy15Wnn0iyyWGKgBDLxf9/s5ABuraCSpBX2uG0jUg4rqJnxsLc5+oYBqoxVaA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/babel__core": "^7.20.5"
@@ -17991,13 +17983,13 @@
}
},
"node_modules/jest-config/node_modules/babel-preset-jest": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.3.0.tgz",
"integrity": "sha512-6ZcUbWHC+dMz2vfzdNwi87Z1gQsLNK2uLuK1Q89R11xdvejcivlYYwDlEv0FHX3VwEXpbBQ9uufB/MUNpZGfhQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.2.0.tgz",
"integrity": "sha512-US4Z3NOieAQumwFnYdUWKvUKh8+YSnS/gB3t6YBiz0bskpu7Pine8pPCheNxlPEW4wnUkma2a94YuW2q3guvCQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"babel-plugin-jest-hoist": "30.3.0",
"babel-plugin-jest-hoist": "30.2.0",
"babel-preset-current-node-syntax": "^1.2.0"
},
"engines": {
@@ -18011,18 +18003,17 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/jest-config/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"devOptional": true,
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -18043,7 +18034,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -18057,10 +18048,10 @@
}
},
"node_modules/jest-config/node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -18070,21 +18061,21 @@
}
},
"node_modules/jest-config/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -18098,38 +18089,38 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-config/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-config/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"devOptional": true,
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -18142,7 +18133,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18152,10 +18143,10 @@
}
},
"node_modules/jest-config/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -18170,14 +18161,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-config/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -18190,7 +18181,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -18200,7 +18191,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -18265,7 +18256,7 @@
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.2.0.tgz",
"integrity": "sha512-tR/FFgZKS1CXluOQzZvNH3+0z9jXr3ldGSD8bhyuxvlVUwbeLOGynkunvlTMxchC5urrKndYiwCFC0DLVjpOCA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"detect-newline": "^3.1.0"
@@ -18275,17 +18266,17 @@
}
},
"node_modules/jest-each": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.3.0.tgz",
"integrity": "sha512-V8eMndg/aZ+3LnCJgSm13IxS5XSBM22QSZc9BtPK8Dek6pm+hfUNfwBdvsB3d342bo1q7wnSkC38zjX259qZNA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.2.0.tgz",
"integrity": "sha512-lpWlJlM7bCUf1mfmuqTA8+j2lNURW9eNafOy99knBM01i5CQeY5UH1vZjgT9071nDJac1M4XsbyI44oNOdhlDQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"chalk": "^4.1.2",
"jest-util": "30.3.0",
"pretty-format": "30.3.0"
"jest-util": "30.2.0",
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18295,7 +18286,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -18305,10 +18296,10 @@
}
},
"node_modules/jest-each/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -18324,17 +18315,17 @@
}
},
"node_modules/jest-each/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-each/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -18344,18 +18335,18 @@
}
},
"node_modules/jest-each/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18365,7 +18356,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18375,10 +18366,10 @@
}
},
"node_modules/jest-each/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -18393,7 +18384,7 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-environment-jsdom": {
@@ -18484,19 +18475,19 @@
}
},
"node_modules/jest-environment-node": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.3.0.tgz",
"integrity": "sha512-4i6HItw/JSiJVsC5q0hnKIe/hbYfZLVG9YJ/0pU9Hz2n/9qZe3Rhn5s5CUZA5ORZlcdT/vmAXRMyONXJwPrmYQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.2.0.tgz",
"integrity": "sha512-ElU8v92QJ9UrYsKrxDIKCxu6PfNj4Hdcktcn0JX12zqNdqWHB0N+hwOnnBBXvjLd2vApZtuLUGs1QSY+MsXoNA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "30.3.0",
"@jest/fake-timers": "30.3.0",
"@jest/types": "30.3.0",
"@jest/environment": "30.2.0",
"@jest/fake-timers": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"jest-mock": "30.3.0",
"jest-util": "30.3.0",
"jest-validate": "30.3.0"
"jest-mock": "30.2.0",
"jest-util": "30.2.0",
"jest-validate": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18506,7 +18497,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -18516,10 +18507,10 @@
}
},
"node_modules/jest-environment-node/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -18535,25 +18526,25 @@
}
},
"node_modules/jest-environment-node/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-environment-node/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18563,7 +18554,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18641,14 +18632,14 @@
}
},
"node_modules/jest-leak-detector": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.3.0.tgz",
"integrity": "sha512-cuKmUUGIjfXZAiGJ7TbEMx0bcqNdPPI6P1V+7aF+m/FUJqFDxkFR4JqkTu8ZOiU5AaX/x0hZ20KaaIPXQzbMGQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.2.0.tgz",
"integrity": "sha512-M6jKAjyzjHG0SrQgwhgZGy9hFazcudwCNovY/9HPIicmNSBuockPSedAP9vlPK6ONFJ1zfyH/M2/YYJxOz5cdQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"pretty-format": "30.3.0"
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18658,7 +18649,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -18668,17 +18659,17 @@
}
},
"node_modules/jest-leak-detector/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-leak-detector/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -18688,10 +18679,10 @@
}
},
"node_modules/jest-leak-detector/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -18706,7 +18697,7 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-matcher-utils": {
@@ -18827,15 +18818,15 @@
}
},
"node_modules/jest-mock": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz",
"integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz",
"integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"jest-util": "30.3.0"
"jest-util": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18845,7 +18836,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -18855,10 +18846,10 @@
}
},
"node_modules/jest-mock/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -18874,25 +18865,25 @@
}
},
"node_modules/jest-mock/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-mock/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18902,7 +18893,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -18940,18 +18931,18 @@
}
},
"node_modules/jest-resolve": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.3.0.tgz",
"integrity": "sha512-NRtTAHQlpd15F9rUR36jqwelbrDV/dY4vzNte3S2kxCKUJRYNd5/6nTSbYiak1VX5g8IoFF23Uj5TURkUW8O5g==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.2.0.tgz",
"integrity": "sha512-TCrHSxPlx3tBY3hWNtRQKbtgLhsXa1WmbJEqBlTBrGafd5fiQFByy2GNCEoGR+Tns8d15GaL9cxEzKOO3GEb2A==",
"dev": true,
"license": "MIT",
"dependencies": {
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-pnp-resolver": "^1.2.3",
"jest-util": "30.3.0",
"jest-validate": "30.3.0",
"jest-util": "30.2.0",
"jest-validate": "30.2.0",
"slash": "^3.0.0",
"unrs-resolver": "^1.7.11"
},
@@ -18960,14 +18951,14 @@
}
},
"node_modules/jest-resolve-dependencies": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.3.0.tgz",
"integrity": "sha512-9ev8s3YN6Hsyz9LV75XUwkCVFlwPbaFn6Wp75qnI0wzAINYWY8Fb3+6y59Rwd3QaS3kKXffHXsZMziMavfz/nw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.2.0.tgz",
"integrity": "sha512-xTOIGug/0RmIe3mmCqCT95yO0vj6JURrn1TKWlNbhiAefJRWINNPgwVkrVgt/YaerPzY3iItufd80v3lOrFJ2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"jest-regex-util": "30.0.1",
"jest-snapshot": "30.3.0"
"jest-snapshot": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18977,7 +18968,7 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -18987,7 +18978,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -18997,10 +18988,10 @@
}
},
"node_modules/jest-resolve/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -19016,28 +19007,28 @@
}
},
"node_modules/jest-resolve/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-resolve/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -19051,25 +19042,25 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-resolve/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -19079,7 +19070,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -19092,39 +19083,39 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
}
},
"node_modules/jest-runner": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.3.0.tgz",
"integrity": "sha512-gDv6C9LGKWDPLia9TSzZwf4h3kMQCqyTpq+95PODnTRDO0g9os48XIYYkS6D236vjpBir2fF63YmJFtqkS5Duw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.2.0.tgz",
"integrity": "sha512-PqvZ2B2XEyPEbclp+gV6KO/F1FIFSbIwewRgmROCMBo/aZ6J1w8Qypoj2pEOcg3G2HzLlaP6VUtvwCI8dM3oqQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/console": "30.3.0",
"@jest/environment": "30.3.0",
"@jest/test-result": "30.3.0",
"@jest/transform": "30.3.0",
"@jest/types": "30.3.0",
"@jest/console": "30.2.0",
"@jest/environment": "30.2.0",
"@jest/test-result": "30.2.0",
"@jest/transform": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
"exit-x": "^0.2.2",
"graceful-fs": "^4.2.11",
"jest-docblock": "30.2.0",
"jest-environment-node": "30.3.0",
"jest-haste-map": "30.3.0",
"jest-leak-detector": "30.3.0",
"jest-message-util": "30.3.0",
"jest-resolve": "30.3.0",
"jest-runtime": "30.3.0",
"jest-util": "30.3.0",
"jest-watcher": "30.3.0",
"jest-worker": "30.3.0",
"jest-environment-node": "30.2.0",
"jest-haste-map": "30.2.0",
"jest-leak-detector": "30.2.0",
"jest-message-util": "30.2.0",
"jest-resolve": "30.2.0",
"jest-runtime": "30.2.0",
"jest-util": "30.2.0",
"jest-watcher": "30.2.0",
"jest-worker": "30.2.0",
"p-limit": "^3.1.0",
"source-map-support": "0.5.13"
},
@@ -19133,21 +19124,21 @@
}
},
"node_modules/jest-runner/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -19167,7 +19158,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -19177,23 +19168,24 @@
}
},
"node_modules/jest-runner/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -19203,10 +19195,10 @@
}
},
"node_modules/jest-runner/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -19222,17 +19214,17 @@
}
},
"node_modules/jest-runner/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-runner/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -19245,7 +19237,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -19265,7 +19257,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -19279,10 +19271,10 @@
}
},
"node_modules/jest-runner/node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -19292,21 +19284,21 @@
}
},
"node_modules/jest-runner/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -19317,19 +19309,19 @@
}
},
"node_modules/jest-runner/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -19341,25 +19333,25 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runner/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -19369,7 +19361,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -19379,10 +19371,10 @@
}
},
"node_modules/jest-runner/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -19397,14 +19389,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-runner/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -19417,7 +19409,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -19427,7 +19419,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -19438,32 +19430,32 @@
}
},
"node_modules/jest-runtime": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.3.0.tgz",
"integrity": "sha512-CgC+hIBJbuh78HEffkhNKcbXAytQViplcl8xupqeIWyKQF50kCQA8J7GeJCkjisC6hpnC9Muf8jV5RdtdFbGng==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.2.0.tgz",
"integrity": "sha512-p1+GVX/PJqTucvsmERPMgCPvQJpFt4hFbM+VN3n8TMo47decMUcJbt+rgzwrEme0MQUA/R+1de2axftTHkKckg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/environment": "30.3.0",
"@jest/fake-timers": "30.3.0",
"@jest/globals": "30.3.0",
"@jest/environment": "30.2.0",
"@jest/fake-timers": "30.2.0",
"@jest/globals": "30.2.0",
"@jest/source-map": "30.0.1",
"@jest/test-result": "30.3.0",
"@jest/transform": "30.3.0",
"@jest/types": "30.3.0",
"@jest/test-result": "30.2.0",
"@jest/transform": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"cjs-module-lexer": "^2.1.0",
"collect-v8-coverage": "^1.0.2",
"glob": "^10.5.0",
"glob": "^10.3.10",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-message-util": "30.3.0",
"jest-mock": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-message-util": "30.2.0",
"jest-mock": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-resolve": "30.3.0",
"jest-snapshot": "30.3.0",
"jest-util": "30.3.0",
"jest-resolve": "30.2.0",
"jest-snapshot": "30.2.0",
"jest-util": "30.2.0",
"slash": "^3.0.0",
"strip-bom": "^4.0.0"
},
@@ -19472,21 +19464,21 @@
}
},
"node_modules/jest-runtime/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -19506,7 +19498,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -19516,23 +19508,24 @@
}
},
"node_modules/jest-runtime/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -19542,10 +19535,10 @@
}
},
"node_modules/jest-runtime/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -19561,17 +19554,17 @@
}
},
"node_modules/jest-runtime/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-runtime/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -19584,7 +19577,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -19604,18 +19597,17 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz",
"integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^1.0.0"
}
},
"node_modules/jest-runtime/node_modules/glob": {
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
"devOptional": true,
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"dev": true,
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -19636,7 +19628,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -19650,10 +19642,10 @@
}
},
"node_modules/jest-runtime/node_modules/istanbul-lib-instrument/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -19663,21 +19655,21 @@
}
},
"node_modules/jest-runtime/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -19688,19 +19680,19 @@
}
},
"node_modules/jest-runtime/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -19712,38 +19704,38 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runtime/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-runtime/node_modules/minimatch": {
"version": "9.0.9",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
"integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
"devOptional": true,
"version": "9.0.5",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz",
"integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==",
"dev": true,
"license": "ISC",
"dependencies": {
"brace-expansion": "^2.0.2"
"brace-expansion": "^2.0.1"
},
"engines": {
"node": ">=16 || 14 >=14.17"
@@ -19756,7 +19748,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -19766,10 +19758,10 @@
}
},
"node_modules/jest-runtime/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -19784,14 +19776,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-runtime/node_modules/signal-exit": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -19804,7 +19796,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -19814,7 +19806,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -19825,10 +19817,10 @@
}
},
"node_modules/jest-snapshot": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.3.0.tgz",
"integrity": "sha512-f14c7atpb4O2DeNhwcvS810Y63wEn8O1HqK/luJ4F6M4NjvxmAKQwBUWjbExUtMxWJQ0wVgmCKymeJK6NZMnfQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.2.0.tgz",
"integrity": "sha512-5WEtTy2jXPFypadKNpbNkZ72puZCa6UjSr/7djeecHWOu7iYhSXSnHScT8wBz3Rn8Ena5d5RYRcsyKIeqG1IyA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
@@ -19836,20 +19828,20 @@
"@babel/plugin-syntax-jsx": "^7.27.1",
"@babel/plugin-syntax-typescript": "^7.27.1",
"@babel/types": "^7.27.3",
"@jest/expect-utils": "30.3.0",
"@jest/expect-utils": "30.2.0",
"@jest/get-type": "30.1.0",
"@jest/snapshot-utils": "30.3.0",
"@jest/transform": "30.3.0",
"@jest/types": "30.3.0",
"@jest/snapshot-utils": "30.2.0",
"@jest/transform": "30.2.0",
"@jest/types": "30.2.0",
"babel-preset-current-node-syntax": "^1.2.0",
"chalk": "^4.1.2",
"expect": "30.3.0",
"expect": "30.2.0",
"graceful-fs": "^4.2.11",
"jest-diff": "30.3.0",
"jest-matcher-utils": "30.3.0",
"jest-message-util": "30.3.0",
"jest-util": "30.3.0",
"pretty-format": "30.3.0",
"jest-diff": "30.2.0",
"jest-matcher-utils": "30.2.0",
"jest-message-util": "30.2.0",
"jest-util": "30.2.0",
"pretty-format": "30.2.0",
"semver": "^7.7.2",
"synckit": "^0.11.8"
},
@@ -19858,21 +19850,21 @@
}
},
"node_modules/jest-snapshot/node_modules/@babel/core": {
"version": "7.29.0",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz",
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"devOptional": true,
"version": "7.28.4",
"resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz",
"integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
"@babel/helper-compilation-targets": "^7.28.6",
"@babel/helper-module-transforms": "^7.28.6",
"@babel/helpers": "^7.28.6",
"@babel/parser": "^7.29.0",
"@babel/template": "^7.28.6",
"@babel/traverse": "^7.29.0",
"@babel/types": "^7.29.0",
"@babel/code-frame": "^7.27.1",
"@babel/generator": "^7.28.3",
"@babel/helper-compilation-targets": "^7.27.2",
"@babel/helper-module-transforms": "^7.28.3",
"@babel/helpers": "^7.28.4",
"@babel/parser": "^7.28.4",
"@babel/template": "^7.27.2",
"@babel/traverse": "^7.28.4",
"@babel/types": "^7.28.4",
"@jridgewell/remapping": "^2.3.5",
"convert-source-map": "^2.0.0",
"debug": "^4.1.0",
@@ -19892,17 +19884,17 @@
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz",
"integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
}
},
"node_modules/jest-snapshot/node_modules/@jest/expect-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.3.0.tgz",
"integrity": "sha512-j0+W5iQQ8hBh7tHZkTQv3q2Fh/M7Je72cIsYqC4OaktgtO7v1So9UTjp6uPBHIaB6beoF/RRsCgMJKvti0wADA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz",
"integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0"
@@ -19915,7 +19907,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -19925,23 +19917,24 @@
}
},
"node_modules/jest-snapshot/node_modules/@jest/transform": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.3.0.tgz",
"integrity": "sha512-TLKY33fSLVd/lKB2YI1pH69ijyUblO/BQvCj566YvnwuzoTNr648iE0j22vRvVNk2HsPwByPxATg3MleS3gf5A==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.2.0.tgz",
"integrity": "sha512-XsauDV82o5qXbhalKxD7p4TZYYdwcaEXC77PPD2HixEFF+6YGppjrAAQurTl2ECWcEomHBMMNS9AH3kcCFx8jA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/core": "^7.27.4",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@jridgewell/trace-mapping": "^0.3.25",
"babel-plugin-istanbul": "^7.0.1",
"chalk": "^4.1.2",
"convert-source-map": "^2.0.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.11",
"jest-haste-map": "30.3.0",
"jest-haste-map": "30.2.0",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"micromatch": "^4.0.8",
"pirates": "^4.0.7",
"slash": "^3.0.0",
"write-file-atomic": "^5.0.1"
@@ -19951,10 +19944,10 @@
}
},
"node_modules/jest-snapshot/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -19970,17 +19963,17 @@
}
},
"node_modules/jest-snapshot/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-snapshot/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -19993,7 +19986,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.1.tgz",
"integrity": "sha512-D8Z6Qm8jCvVXtIRkBnqNHX0zJ37rQcFJ9u8WOS6tkYOsRdHBzypCstaxWiu5ZIlqQtviRYbgnRLSoCEvjqcqbA==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"workspaces": [
"test/babel-8"
@@ -20010,18 +20003,18 @@
}
},
"node_modules/jest-snapshot/node_modules/expect": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.3.0.tgz",
"integrity": "sha512-1zQrciTiQfRdo7qJM1uG4navm8DayFa2TgCSRlzUyNkhcJ6XUZF3hjnpkyr3VhAqPH7i/9GkG7Tv5abz6fqz0Q==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz",
"integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/expect-utils": "30.3.0",
"@jest/expect-utils": "30.2.0",
"@jest/get-type": "30.1.0",
"jest-matcher-utils": "30.3.0",
"jest-message-util": "30.3.0",
"jest-mock": "30.3.0",
"jest-util": "30.3.0"
"jest-matcher-utils": "30.2.0",
"jest-message-util": "30.2.0",
"jest-mock": "30.2.0",
"jest-util": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -20031,7 +20024,7 @@
"version": "6.0.3",
"resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz",
"integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==",
"devOptional": true,
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"@babel/core": "^7.23.9",
@@ -20045,37 +20038,37 @@
}
},
"node_modules/jest-snapshot/node_modules/jest-diff": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.3.0.tgz",
"integrity": "sha512-n3q4PDQjS4LrKxfWB3Z5KNk1XjXtZTBwQp71OP0Jo03Z6V60x++K5L8k6ZrW8MY8pOFylZvHM0zsjS1RqlHJZQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz",
"integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/diff-sequences": "30.3.0",
"@jest/diff-sequences": "30.0.1",
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"pretty-format": "30.3.0"
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-snapshot/node_modules/jest-haste-map": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.3.0.tgz",
"integrity": "sha512-mMi2oqG4KRU0R9QEtscl87JzMXfUhbKaFqOxmjb2CKcbHcUGFrJCBWHmnTiUqi6JcnzoBlO4rWfpdl2k/RfLCA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.2.0.tgz",
"integrity": "sha512-sQA/jCb9kNt+neM0anSj6eZhLZUIhQgwDt7cPGjumgLM4rXsfb9kpnlacmvZz3Q5tb80nS+oG/if+NBKrHC+Xw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"anymatch": "^3.1.3",
"fb-watchman": "^2.0.2",
"graceful-fs": "^4.2.11",
"jest-regex-util": "30.0.1",
"jest-util": "30.3.0",
"jest-worker": "30.3.0",
"picomatch": "^4.0.3",
"jest-util": "30.2.0",
"jest-worker": "30.2.0",
"micromatch": "^4.0.8",
"walker": "^1.0.8"
},
"engines": {
@@ -20086,35 +20079,35 @@
}
},
"node_modules/jest-snapshot/node_modules/jest-matcher-utils": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.3.0.tgz",
"integrity": "sha512-HEtc9uFQgaUHkC7nLSlQL3Tph4Pjxt/yiPvkIrrDCt9jhoLIgxaubo1G+CFOnmHYMxHwwdaSN7mkIFs6ZK8OhA==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz",
"integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"chalk": "^4.1.2",
"jest-diff": "30.3.0",
"pretty-format": "30.3.0"
"jest-diff": "30.2.0",
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-snapshot/node_modules/jest-message-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz",
"integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz",
"integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@babel/code-frame": "^7.27.1",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/stack-utils": "^2.0.3",
"chalk": "^4.1.2",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3",
"pretty-format": "30.3.0",
"micromatch": "^4.0.8",
"pretty-format": "30.2.0",
"slash": "^3.0.0",
"stack-utils": "^2.0.6"
},
@@ -20126,25 +20119,25 @@
"version": "30.0.1",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz",
"integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
}
},
"node_modules/jest-snapshot/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -20154,7 +20147,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -20164,10 +20157,10 @@
}
},
"node_modules/jest-snapshot/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -20182,14 +20175,14 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-snapshot/node_modules/semver": {
"version": "7.7.4",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
"integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
"devOptional": true,
"version": "7.7.2",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz",
"integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==",
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -20202,7 +20195,7 @@
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
"integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"engines": {
"node": ">=14"
@@ -20215,7 +20208,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
"integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -20225,7 +20218,7 @@
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
"integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
"devOptional": true,
"dev": true,
"license": "ISC",
"dependencies": {
"imurmurhash": "^0.1.4",
@@ -20270,18 +20263,18 @@
}
},
"node_modules/jest-validate": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.3.0.tgz",
"integrity": "sha512-I/xzC8h5G+SHCb2P2gWkJYrNiTbeL47KvKeW5EzplkyxzBRBw1ssSHlI/jXec0ukH2q7x2zAWQm7015iusg62Q==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.2.0.tgz",
"integrity": "sha512-FBGWi7dP2hpdi8nBoWxSsLvBFewKAg0+uSQwBaof4Y4DPgBabXgpSYC5/lR7VmnIlSpASmCi/ntRWPbv7089Pw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/get-type": "30.1.0",
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"camelcase": "^6.3.0",
"chalk": "^4.1.2",
"leven": "^3.1.0",
"pretty-format": "30.3.0"
"pretty-format": "30.2.0"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -20291,7 +20284,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -20301,10 +20294,10 @@
}
},
"node_modules/jest-validate/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -20320,17 +20313,17 @@
}
},
"node_modules/jest-validate/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-validate/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=10"
@@ -20340,10 +20333,10 @@
}
},
"node_modules/jest-validate/node_modules/pretty-format": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz",
"integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz",
"integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/schemas": "30.0.5",
@@ -20358,23 +20351,23 @@
"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==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/jest-watcher": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.3.0.tgz",
"integrity": "sha512-PJ1d9ThtTR8aMiBWUdcownq9mDdLXsQzJayTk4kmaBRHKvwNQn+ANveuhEBUyNI2hR1TVhvQ8D5kHubbzBHR/w==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.2.0.tgz",
"integrity": "sha512-PYxa28dxJ9g777pGm/7PrbnMeA0Jr7osHP9bS7eJy9DuAjMgdGtxgf0uKMyoIsTWAkIbUW5hSDdJ3urmgXBqxg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/test-result": "30.3.0",
"@jest/types": "30.3.0",
"@jest/test-result": "30.2.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"ansi-escapes": "^4.3.2",
"chalk": "^4.1.2",
"emittery": "^0.13.1",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"string-length": "^4.0.2"
},
"engines": {
@@ -20385,7 +20378,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -20395,10 +20388,10 @@
}
},
"node_modules/jest-watcher/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -20414,25 +20407,25 @@
}
},
"node_modules/jest-watcher/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-watcher/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -20442,7 +20435,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -20452,15 +20445,15 @@
}
},
"node_modules/jest-worker": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.3.0.tgz",
"integrity": "sha512-DrCKkaQwHexjRUFTmPzs7sHQe0TSj9nvDALKGdwmK5mW9v7j90BudWirKAJHt3QQ9Dhrg1F7DogPzhChppkJpQ==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.2.0.tgz",
"integrity": "sha512-0Q4Uk8WF7BUwqXHuAjc23vmopWJw5WH7w2tqBoUOZpOjW/ZnR44GXXd1r82RvnmI2GZge3ivrYXk/BE2+VtW2g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
"@ungap/structured-clone": "^1.3.0",
"jest-util": "30.3.0",
"jest-util": "30.2.0",
"merge-stream": "^2.0.0",
"supports-color": "^8.1.1"
},
@@ -20472,7 +20465,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -20482,10 +20475,10 @@
}
},
"node_modules/jest-worker/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -20501,25 +20494,25 @@
}
},
"node_modules/jest-worker/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jest-worker/node_modules/jest-util": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz",
"integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz",
"integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/types": "30.3.0",
"@jest/types": "30.2.0",
"@types/node": "*",
"chalk": "^4.1.2",
"ci-info": "^4.2.0",
"graceful-fs": "^4.2.11",
"picomatch": "^4.0.3"
"picomatch": "^4.0.2"
},
"engines": {
"node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0"
@@ -20529,7 +20522,7 @@
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz",
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12"
@@ -20542,7 +20535,7 @@
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@@ -20558,7 +20551,7 @@
"version": "30.0.5",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz",
"integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==",
"devOptional": true,
"dev": true,
"license": "MIT",
"dependencies": {
"@sinclair/typebox": "^0.34.0"
@@ -20568,10 +20561,10 @@
}
},
"node_modules/jest/node_modules/@jest/types": {
"version": "30.3.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz",
"integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==",
"devOptional": true,
"version": "30.2.0",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz",
"integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@jest/pattern": "30.0.1",
@@ -20587,10 +20580,10 @@
}
},
"node_modules/jest/node_modules/@sinclair/typebox": {
"version": "0.34.48",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz",
"integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==",
"devOptional": true,
"version": "0.34.41",
"resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz",
"integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==",
"dev": true,
"license": "MIT"
},
"node_modules/jiti": {
@@ -21006,6 +20999,12 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -21349,16 +21348,6 @@
"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"
}
},
"node_modules/mini-css-extract-plugin": {
"version": "1.6.2",
"resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz",
@@ -21652,9 +21641,9 @@
}
},
"node_modules/object-code": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/object-code/-/object-code-2.0.0.tgz",
"integrity": "sha512-qOwMF43O/VAD51nJAB7MKsf1yWksql6O1i0DHRo1yaOQM6xJQH0NAE9UKJzYB7lyKw1jnpeb2BmB8qakjxiYZA==",
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/object-code/-/object-code-1.3.3.tgz",
"integrity": "sha512-/Ds4Xd5xzrtUOJ+xJQ57iAy0BZsZltOHssnDgcZ8DOhgh41q1YJCnTPnWdWSLkNGNnxYzhYChjc5dgC9mEERCA==",
"license": "MIT"
},
"node_modules/object-filter": {
@@ -22587,6 +22576,7 @@
}
],
"license": "MIT",
"peer": true,
"dependencies": {
"nanoid": "^3.3.7",
"picocolors": "^1.1.1",
@@ -23371,7 +23361,6 @@
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-27.5.1.tgz",
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -23386,7 +23375,6 @@
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -23398,8 +23386,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/process": {
"version": "0.11.10",
@@ -23436,6 +23423,7 @@
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
"integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.4.0",
"object-assign": "^4.1.1",
@@ -23508,7 +23496,7 @@
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz",
"integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==",
"devOptional": true,
"dev": true,
"funding": [
{
"type": "individual",
@@ -23693,6 +23681,7 @@
"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"
},
@@ -23851,6 +23840,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
"integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
"license": "MIT",
"peer": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.2"
@@ -23888,19 +23878,6 @@
"node": ">= 12"
}
},
"node_modules/react-error-boundary": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-4.1.2.tgz",
"integrity": "sha512-GQDxZ5Jd+Aq/qUxbCm1UtzmL/s++V7zKgE8yMktJiCQXCCFZnMZh9ng+6/Ne6PjNSXH0L9CjeOEREfRnq6Duag==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/runtime": "^7.12.5"
},
"peerDependencies": {
"react": ">=16.13.1"
}
},
"node_modules/react-error-overlay": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.1.0.tgz",
@@ -24062,6 +24039,16 @@
"tslib": "2"
}
},
"node_modules/react-intl/node_modules/@types/react": {
"version": "18.3.24",
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.24.tgz",
"integrity": "sha512-0dLEBsA1kI3OezMBF8nSsb7Nk19ZnsyE1LLhB8r27KbgU5H4pvuqZLdtE+aUkJVoXgTVuA+iLIwmZ0TuK4tx6A==",
"license": "MIT",
"dependencies": {
"@types/prop-types": "*",
"csstype": "^3.0.2"
}
},
"node_modules/react-is": {
"version": "16.13.1",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
@@ -24154,8 +24141,7 @@
"version": "17.0.2",
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"license": "MIT",
"peer": true
"license": "MIT"
},
"node_modules/react-refresh": {
"version": "0.16.0",
@@ -24163,6 +24149,7 @@
"integrity": "sha512-FPvF2XxTSikpJxcr+bHut2H4gJ17+18Uy20D5/F+SKzFap62R3cM5wH6b8WN3LyGSYeQilLEcJcR1fjBSI2S1A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -24233,12 +24220,12 @@
}
},
"node_modules/react-router": {
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.3.tgz",
"integrity": "sha512-XRnlbKMTmktBkjCLE8/XcZFlnHvr2Ltdr1eJX4idL55/9BbORzyZEaIkBFDhFGCEWBBItsVrDxwx3gnisMitdw==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router/-/react-router-6.30.1.tgz",
"integrity": "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ==",
"license": "MIT",
"dependencies": {
"@remix-run/router": "1.23.2"
"@remix-run/router": "1.23.0"
},
"engines": {
"node": ">=14.0.0"
@@ -24248,13 +24235,14 @@
}
},
"node_modules/react-router-dom": {
"version": "6.30.3",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.3.tgz",
"integrity": "sha512-pxPcv1AczD4vso7G4Z3TKcvlxK7g7TNt3/FNGMhfqyntocvYKj+GCatfigGDjbLozC4baguJ0ReCigoDJXb0ag==",
"version": "6.30.1",
"resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-6.30.1.tgz",
"integrity": "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@remix-run/router": "1.23.2",
"react-router": "6.30.3"
"@remix-run/router": "1.23.0",
"react-router": "6.30.1"
},
"engines": {
"node": ">=14.0.0"
@@ -24440,20 +24428,6 @@
"node": ">=6.0.0"
}
},
"node_modules/redent": {
"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",
"strip-indent": "^3.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/reduce-function-call": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/reduce-function-call/-/reduce-function-call-1.0.3.tgz",
@@ -24468,10 +24442,50 @@
"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"
}
},
"node_modules/redux-logger": {
"version": "3.0.6",
"resolved": "https://registry.npmjs.org/redux-logger/-/redux-logger-3.0.6.tgz",
"integrity": "sha512-JoCIok7bg/XpqA1JqCqXFypuqBbQzGQySrhFzewB7ThcnysTO30l4VCst86AuB9T9tuT03MAA56Jw2PNhRSNCg==",
"license": "MIT",
"dependencies": {
"deep-diff": "^0.3.5"
}
},
"node_modules/redux-mock-store": {
"version": "1.5.5",
"resolved": "https://registry.npmjs.org/redux-mock-store/-/redux-mock-store-1.5.5.tgz",
"integrity": "sha512-YxX+ofKUTQkZE4HbhYG4kKGr7oCTJfB0GLy7bSeqx86GLpGirrbUWstMnqXkqHNaQpcnbMGbof2dYs5KsPE6Zg==",
"license": "MIT",
"dependencies": {
"lodash.isplainobject": "^4.0.6"
},
"peerDependencies": {
"redux": "*"
}
},
"node_modules/redux-saga": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/redux-saga/-/redux-saga-1.3.0.tgz",
"integrity": "sha512-J9RvCeAZXSTAibFY0kGw6Iy4EdyDNW7k6Q+liwX+bsck7QVsU78zz8vpBRweEfANxnnlG/xGGeOvf6r8UXzNJQ==",
"license": "MIT",
"dependencies": {
"@redux-saga/core": "^1.3.0"
}
},
"node_modules/redux-thunk": {
"version": "2.4.2",
"resolved": "https://registry.npmjs.org/redux-thunk/-/redux-thunk-2.4.2.tgz",
"integrity": "sha512-+P3TjtnP0k/FEjcBL5FZpoovtvrTNT/UXd4/sluaSyrURlSlhLSzEdfsTBW7WsKB6yPvgd7q/iZPICFjW4o57Q==",
"license": "MIT",
"peerDependencies": {
"redux": "^4"
}
},
"node_modules/reflect.getprototypeof": {
"version": "1.0.10",
"resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz",
@@ -24651,6 +24665,12 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/reselect": {
"version": "5.1.1",
"resolved": "https://registry.npmjs.org/reselect/-/reselect-5.1.1.tgz",
"integrity": "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==",
"license": "MIT"
},
"node_modules/resolve": {
"version": "1.22.10",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz",
@@ -25082,6 +25102,7 @@
"integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"fast-deep-equal": "^3.1.3",
"fast-uri": "^3.0.1",
@@ -26064,19 +26085,6 @@
"node": ">=6"
}
},
"node_modules/strip-indent": {
"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"
},
"engines": {
"node": ">=8"
}
},
"node_modules/strip-json-comments": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
@@ -26096,6 +26104,7 @@
"integrity": "sha512-+xU0IA1StzqAqFs/QtXkK+XJa7wpS4X5H+JQccRKsRCElgeLGocFU1U/UMvMUylKFw6vwGV+Y/a2wb2pm5rFFQ==",
"hasInstallScript": true,
"license": "Apache-2.0",
"peer": true,
"dependencies": {
"@bundled-es-modules/deepmerge": "^4.3.1",
"@bundled-es-modules/glob": "^10.4.2",
@@ -26355,10 +26364,10 @@
"license": "MIT"
},
"node_modules/synckit": {
"version": "0.11.12",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz",
"integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==",
"devOptional": true,
"version": "0.11.11",
"resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz",
"integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@pkgr/core": "^0.2.9"
@@ -26613,6 +26622,7 @@
"integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==",
"devOptional": true,
"license": "MIT",
"peer": true,
"engines": {
"node": ">=12"
},
@@ -26726,15 +26736,6 @@
"tslib": "2"
}
},
"node_modules/try": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/try/-/try-1.0.3.tgz",
"integrity": "sha512-AHA8khVCII6zKyRkyPo6pRwoR9v5jb7QFw6e5avtaVSkxVfaEucYIo06xnwB+pJaEarfYNbs7W3Vq+LZLZiWyA==",
"license": "MIT",
"funding": {
"url": "https://github.com/arthurfiorette/try?sponsor=1"
}
},
"node_modules/ts-api-utils": {
"version": "1.4.3",
"resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz",
@@ -26749,11 +26750,12 @@
}
},
"node_modules/ts-jest": {
"version": "29.4.6",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz",
"integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==",
"devOptional": true,
"version": "29.4.5",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.5.tgz",
"integrity": "sha512-HO3GyiWn2qvTQA4kTgjDcXiMwYQt68a1Y8+JuLRVpdIzm+UOLSHgl/XqR4c6nzJkq5rOkjc02O2I7P7l/Yof0Q==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"bs-logger": "^0.2.6",
"fast-json-stable-stringify": "^2.1.0",
@@ -26805,7 +26807,7 @@
"version": "7.7.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
"integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
"devOptional": true,
"dev": true,
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
@@ -26818,7 +26820,7 @@
"version": "4.41.0",
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
"integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
"devOptional": true,
"dev": true,
"license": "(MIT OR CC0-1.0)",
"engines": {
"node": ">=16"
@@ -26908,7 +26910,8 @@
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
"license": "0BSD",
"peer": true
},
"node_modules/tsutils": {
"version": "3.21.0",
@@ -26962,6 +26965,7 @@
"integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
"devOptional": true,
"license": "(MIT OR CC0-1.0)",
"peer": true,
"engines": {
"node": ">=10"
},
@@ -27067,6 +27071,7 @@
"integrity": "sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -27075,10 +27080,35 @@
"node": ">=4.2.0"
}
},
"node_modules/typescript-compare": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/typescript-compare/-/typescript-compare-0.0.2.tgz",
"integrity": "sha512-8ja4j7pMHkfLJQO2/8tut7ub+J3Lw2S3061eJLFQcvs3tsmJKp8KG5NtpLn7KcY2w08edF74BSVN7qJS0U6oHA==",
"license": "MIT",
"dependencies": {
"typescript-logic": "^0.0.0"
}
},
"node_modules/typescript-logic": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/typescript-logic/-/typescript-logic-0.0.0.tgz",
"integrity": "sha512-zXFars5LUkI3zP492ls0VskH3TtdeHCqu0i7/duGt60i5IGPIpAHE/DWo5FqJ6EjQ15YKXrt+AETjv60Dat34Q==",
"license": "MIT"
},
"node_modules/typescript-tuple": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/typescript-tuple/-/typescript-tuple-2.2.1.tgz",
"integrity": "sha512-Zcr0lbt8z5ZdEzERHAMAniTiIKerFCMgd7yjq1fPnDJ43et/k9twIFQMUYff9k5oXcsQ0WpvFcgzK2ZKASoW6Q==",
"license": "MIT",
"dependencies": {
"typescript-compare": "^0.0.2"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"dev": true,
"license": "BSD-2-Clause",
"optional": true,
"bin": {
@@ -27228,6 +27258,7 @@
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"peer": true,
"dependencies": {
"napi-postinstall": "^0.3.0"
},
@@ -27417,15 +27448,6 @@
}
}
},
"node_modules/use-sync-external-store": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
"integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
"license": "MIT",
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/util": {
"version": "0.12.5",
"resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz",
@@ -27596,6 +27618,7 @@
"integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.8",
@@ -27704,6 +27727,7 @@
"integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@discoveryjs/json-ext": "^0.5.0",
"@webpack-cli/configtest": "^2.1.1",
@@ -27784,6 +27808,7 @@
"integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==",
"devOptional": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@types/bonjour": "^3.5.9",
"@types/connect-history-api-fallback": "^1.3.5",
@@ -28130,7 +28155,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"devOptional": true,
"dev": true,
"license": "MIT"
},
"node_modules/wrap-ansi": {

View File

@@ -36,10 +36,9 @@
"@fortawesome/free-brands-svg-icons": "6.7.2",
"@fortawesome/free-solid-svg-icons": "6.7.2",
"@fortawesome/react-fontawesome": "0.2.6",
"@openedx/frontend-plugin-framework": "^1.7.0",
"@openedx/paragon": "^23.4.2",
"@optimizely/react-sdk": "^2.9.1",
"@tanstack/react-query": "^5.90.19",
"@redux-devtools/extension": "3.3.0",
"@testing-library/react": "^16.2.0",
"algoliasearch": "^4.14.3",
"algoliasearch-helper": "^3.26.0",
@@ -53,23 +52,28 @@
"react-dom": "^18.3.1",
"react-helmet": "6.1.0",
"react-loading-skeleton": "3.5.0",
"react-redux": "7.2.9",
"react-responsive": "8.2.0",
"react-router": "6.30.3",
"react-router-dom": "6.30.3",
"react-router": "6.30.1",
"react-router-dom": "6.30.1",
"react-zendesk": "^0.1.13",
"redux": "4.2.1",
"redux-logger": "3.0.6",
"redux-mock-store": "1.5.5",
"redux-saga": "1.3.0",
"redux-thunk": "2.4.2",
"regenerator-runtime": "0.14.1",
"reselect": "5.1.1",
"universal-cookie": "7.2.2"
},
"devDependencies": {
"@edx/browserslist-config": "^1.1.1",
"@edx/typescript-config": "^1.1.0",
"@openedx/frontend-build": "^14.6.2",
"@testing-library/jest-dom": "^6.9.1",
"babel-plugin-formatjs": "10.5.41",
"eslint-plugin-import": "2.32.0",
"glob": "7.2.3",
"history": "5.3.0",
"jest": "30.3.0",
"jest": "30.2.0",
"react-test-renderer": "^18.3.1",
"ts-jest": "^29.4.0"
}

View File

@@ -1,12 +1,14 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { AppProvider } from '@edx/frontend-platform/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { Helmet } from 'react-helmet';
import { Navigate, Route, Routes } from 'react-router-dom';
import {
EmbeddedRegistrationRoute, NotFoundPage, registerIcons, UnAuthOnlyRoute, Zendesk,
} from './common-components';
import configureStore from './data/configureStore';
import {
AUTHN_PROGRESSIVE_PROFILING,
LOGIN_PAGE,
@@ -29,48 +31,33 @@ import './index.scss';
registerIcons();
const queryClient = new QueryClient({
defaultOptions: {
mutations: {
retry: false,
},
},
});
const MainApp = () => (
<QueryClientProvider client={queryClient}>
<AppProvider>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route
path={REGISTER_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={REGISTER_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
</AppProvider>
</QueryClientProvider>
<AppProvider store={configureStore()}>
<Helmet>
<link rel="shortcut icon" href={getConfig().FAVICON_URL} type="image/x-icon" />
</Helmet>
{getConfig().ZENDESK_KEY && <Zendesk />}
<Routes>
<Route path="/" element={<Navigate replace to={updatePathWithQueryParams(REGISTER_PAGE)} />} />
<Route
path={REGISTER_EMBEDDED_PAGE}
element={<EmbeddedRegistrationRoute><RegistrationPage /></EmbeddedRegistrationRoute>}
/>
<Route
path={LOGIN_PAGE}
element={
<UnAuthOnlyRoute><Logistration selectedPage={LOGIN_PAGE} /></UnAuthOnlyRoute>
}
/>
<Route path={REGISTER_PAGE} element={<UnAuthOnlyRoute><Logistration /></UnAuthOnlyRoute>} />
<Route path={RESET_PAGE} element={<UnAuthOnlyRoute><ForgotPasswordPage /></UnAuthOnlyRoute>} />
<Route path={PASSWORD_RESET_CONFIRM} element={<ResetPasswordPage />} />
<Route path={AUTHN_PROGRESSIVE_PROFILING} element={<ProgressiveProfiling />} />
<Route path={RECOMMENDATIONS} element={<RecommendationsPage />} />
<Route path={PAGE_NOT_FOUND} element={<NotFoundPage />} />
<Route path="*" element={<Navigate replace to={PAGE_NOT_FOUND} />} />
</Routes>
</AppProvider>
);
export default MainApp;

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render, screen } from '@testing-library/react';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Hyperlink, Image } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { breakpoints } from '@openedx/paragon';
import classNames from 'classnames';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { mergeConfig } from '@edx/frontend-platform';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { render } from '@testing-library/react';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Navigate } from 'react-router-dom';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import React, { useState } from 'react';
import {
Form, TransitionReplace,

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Button, Hyperlink, Icon } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { FormattedMessage } from '@edx/frontend-platform/i18n';
const NotFoundPage = () => (

View File

@@ -1,4 +1,5 @@
import { useState } from 'react';
import React, { useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import {
@@ -11,31 +12,17 @@ import PropTypes from 'prop-types';
import messages from './messages';
import { LETTER_REGEX, NUMBER_REGEX } from '../data/constants';
import { useRegisterContext } from '../register/components/RegisterContext';
import { useFieldValidations } from '../register/data/apiHook';
import { clearRegistrationBackendError, fetchRealtimeValidations } from '../register/data/actions';
import { validatePasswordField } from '../register/data/utils';
const PasswordField = (props) => {
const { formatMessage } = useIntl();
const dispatch = useDispatch();
const validationApiRateLimited = useSelector(state => state.register.validationApiRateLimited);
const [isPasswordHidden, setHiddenTrue, setHiddenFalse] = useToggle(true);
const [showTooltip, setShowTooltip] = useState(false);
const {
setValidationsSuccess,
setValidationsFailure,
validationApiRateLimited,
clearRegistrationBackendError,
} = useRegisterContext();
const fieldValidationsMutation = useFieldValidations({
onSuccess: (data) => {
setValidationsSuccess(data);
},
onError: () => {
setValidationsFailure();
},
});
const handleBlur = (e) => {
const { name, value } = e.target;
if (name === props.name && e.relatedTarget?.name === 'passwordIcon') {
@@ -63,7 +50,7 @@ const PasswordField = (props) => {
if (fieldError) {
props.handleErrorChange('password', fieldError);
} else if (!validationApiRateLimited) {
fieldValidationsMutation.mutate({ password: passwordValue });
dispatch(fetchRealtimeValidations({ password: passwordValue }));
}
}
};
@@ -78,7 +65,7 @@ const PasswordField = (props) => {
}
if (props.handleErrorChange) {
props.handleErrorChange('password', '');
clearRegistrationBackendError('password');
dispatch(clearRegistrationBackendError('password'));
}
setTimeout(() => setShowTooltip(props.showRequirements && true), 150);
};

View File

@@ -22,6 +22,7 @@ const RedirectLogistration = (props) => {
host,
} = props;
let finalRedirectUrl = '';
if (success) {
// If we're in a third party auth pipeline, we must complete the pipeline
// once user has successfully logged in. Otherwise, redirect to the specified redirect url.

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import {

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
import Zendesk from 'react-zendesk';

View File

@@ -1,61 +0,0 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from './ThirdPartyAuthContext';
const TestComponent = () => {
const {
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
} = useThirdPartyAuthContext();
return (
<div>
<div>{fieldDescriptions ? 'FieldDescriptions Available' : 'FieldDescriptions Not Available'}</div>
<div>{optionalFields ? 'OptionalFields Available' : 'OptionalFields Not Available'}</div>
<div>{thirdPartyAuthApiStatus !== null ? 'AuthApiStatus Available' : 'AuthApiStatus Not Available'}</div>
<div>{thirdPartyAuthContext ? 'AuthContext Available' : 'AuthContext Not Available'}</div>
</div>
);
};
describe('ThirdPartyAuthContext', () => {
it('should render children', () => {
render(
<ThirdPartyAuthProvider>
<div>Test Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('should provide all context values to children', () => {
render(
<ThirdPartyAuthProvider>
<TestComponent />
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('FieldDescriptions Available')).toBeInTheDocument();
expect(screen.getByText('OptionalFields Available')).toBeInTheDocument();
expect(screen.getByText('AuthApiStatus Not Available')).toBeInTheDocument(); // Initially null
expect(screen.getByText('AuthContext Available')).toBeInTheDocument();
});
it('should render multiple children', () => {
render(
<ThirdPartyAuthProvider>
<div>First Child</div>
<div>Second Child</div>
<div>Third Child</div>
</ThirdPartyAuthProvider>,
);
expect(screen.getByText('First Child')).toBeInTheDocument();
expect(screen.getByText('Second Child')).toBeInTheDocument();
expect(screen.getByText('Third Child')).toBeInTheDocument();
});
});

View File

@@ -1,133 +0,0 @@
import {
createContext, FC, ReactNode, useCallback, useContext, useMemo, useState,
} from 'react';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
interface ThirdPartyAuthContextType {
fieldDescriptions: any;
optionalFields: {
fields: any;
extended_profile: any[];
};
thirdPartyAuthApiStatus: string | null;
thirdPartyAuthContext: {
platformName: string | null;
autoSubmitRegForm: boolean;
currentProvider: string | null;
finishAuthUrl: string | null;
countryCode: string | null;
providers: any[];
secondaryProviders: any[];
pipelineUserDetails: any | null;
errorMessage: string | null;
welcomePageRedirectUrl: string | null;
};
setThirdPartyAuthContextBegin: () => void;
setThirdPartyAuthContextSuccess: (fieldDescData: any, optionalFieldsData: any, contextData: any) => void;
setThirdPartyAuthContextFailure: () => void;
clearThirdPartyAuthErrorMessage: () => void;
}
const ThirdPartyAuthContext = createContext<ThirdPartyAuthContextType | undefined>(undefined);
interface ThirdPartyAuthProviderProps {
children: ReactNode;
}
export const ThirdPartyAuthProvider: FC<ThirdPartyAuthProviderProps> = ({ children }) => {
const [fieldDescriptions, setFieldDescriptions] = useState({});
const [optionalFields, setOptionalFields] = useState({
fields: {},
extended_profile: [],
});
const [thirdPartyAuthApiStatus, setThirdPartyAuthApiStatus] = useState<string | null>(null);
const [thirdPartyAuthContext, setThirdPartyAuthContext] = useState({
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
// Function to handle begin state - mirrors THIRD_PARTY_AUTH_CONTEXT.BEGIN
const setThirdPartyAuthContextBegin = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
}, []);
// Function to handle success - mirrors THIRD_PARTY_AUTH_CONTEXT.SUCCESS
const setThirdPartyAuthContextSuccess = useCallback((fieldDescData, optionalFieldsData, contextData) => {
setFieldDescriptions(fieldDescData?.fields || {});
setOptionalFields(optionalFieldsData || { fields: {}, extended_profile: [] });
setThirdPartyAuthContext(contextData || {
platformName: null,
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
});
setThirdPartyAuthApiStatus(COMPLETE_STATE);
}, []);
// Function to handle failure - mirrors THIRD_PARTY_AUTH_CONTEXT.FAILURE
const setThirdPartyAuthContextFailure = useCallback(() => {
setThirdPartyAuthApiStatus(FAILURE_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
// Function to clear error message - mirrors THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG
const clearThirdPartyAuthErrorMessage = useCallback(() => {
setThirdPartyAuthApiStatus(PENDING_STATE);
setThirdPartyAuthContext(prev => ({
...prev,
errorMessage: null,
}));
}, []);
const value = useMemo(() => ({
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
}), [
fieldDescriptions,
optionalFields,
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
clearThirdPartyAuthErrorMessage,
]);
return (
<ThirdPartyAuthContext.Provider value={value}>
{children}
</ThirdPartyAuthContext.Provider>
);
};
export const useThirdPartyAuthContext = (): ThirdPartyAuthContextType => {
const context = useContext(ThirdPartyAuthContext);
if (context === undefined) {
throw new Error('useThirdPartyAuthContext must be used within a ThirdPartyAuthProvider');
}
return context;
};

View File

@@ -0,0 +1,27 @@
import { AsyncActionType } from '../../data/utils';
export const THIRD_PARTY_AUTH_CONTEXT = new AsyncActionType('THIRD_PARTY_AUTH', 'GET_THIRD_PARTY_AUTH_CONTEXT');
export const THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG = 'THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG';
// Third party auth context
export const getThirdPartyAuthContext = (urlParams) => ({
type: THIRD_PARTY_AUTH_CONTEXT.BASE,
payload: { urlParams },
});
export const getThirdPartyAuthContextBegin = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.BEGIN,
});
export const getThirdPartyAuthContextSuccess = (fieldDescriptions, optionalFields, thirdPartyAuthContext) => ({
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
});
export const getThirdPartyAuthContextFailure = () => ({
type: THIRD_PARTY_AUTH_CONTEXT.FAILURE,
});
export const clearThirdPartyAuthContextErrorMessage = () => ({
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
});

View File

@@ -1,17 +0,0 @@
import { useQuery } from '@tanstack/react-query';
import { getThirdPartyAuthContext } from './api';
import { ThirdPartyAuthQueryKeys } from './queryKeys';
// Error constants
export const THIRD_PARTY_AUTH_ERROR = 'third-party-auth-error';
const useThirdPartyAuthHook = (pageId, payload) => useQuery({
queryKey: ThirdPartyAuthQueryKeys.byPage(pageId),
queryFn: () => getThirdPartyAuthContext(payload),
retry: false,
});
export {
useThirdPartyAuthHook,
};

View File

@@ -1,6 +0,0 @@
import { appId } from '../../constants';
export const ThirdPartyAuthQueryKeys = {
all: [appId, 'ThirdPartyAuth'] as const,
byPage: (pageId: string) => [appId, 'ThirdPartyAuth', pageId] as const,
};

View File

@@ -0,0 +1,63 @@
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from './actions';
import { COMPLETE_STATE, FAILURE_STATE, PENDING_STATE } from '../../data/constants';
export const defaultState = {
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
},
};
const reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case THIRD_PARTY_AUTH_CONTEXT.BEGIN:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
};
case THIRD_PARTY_AUTH_CONTEXT.SUCCESS: {
return {
...state,
fieldDescriptions: action.payload.fieldDescriptions?.fields,
optionalFields: action.payload.optionalFields,
thirdPartyAuthContext: action.payload.thirdPartyAuthContext,
thirdPartyAuthApiStatus: COMPLETE_STATE,
};
}
case THIRD_PARTY_AUTH_CONTEXT.FAILURE:
return {
...state,
thirdPartyAuthApiStatus: FAILURE_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
case THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG:
return {
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,32 @@
import { logError } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
getThirdPartyAuthContextBegin,
getThirdPartyAuthContextFailure,
getThirdPartyAuthContextSuccess,
THIRD_PARTY_AUTH_CONTEXT,
} from './actions';
import {
getThirdPartyAuthContext,
} from './service';
import { setCountryFromThirdPartyAuthContext } from '../../register/data/actions';
export function* fetchThirdPartyAuthContext(action) {
try {
yield put(getThirdPartyAuthContextBegin());
const {
fieldDescriptions, optionalFields, thirdPartyAuthContext,
} = yield call(getThirdPartyAuthContext, action.payload.urlParams);
yield put(setCountryFromThirdPartyAuthContext(thirdPartyAuthContext.countryCode));
yield put(getThirdPartyAuthContextSuccess(fieldDescriptions, optionalFields, thirdPartyAuthContext));
} catch (e) {
yield put(getThirdPartyAuthContextFailure());
logError(e);
}
}
export default function* saga() {
yield takeEvery(THIRD_PARTY_AUTH_CONTEXT.BASE, fetchThirdPartyAuthContext);
}

View File

@@ -0,0 +1,28 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const thirdPartyAuthContextSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.thirdPartyAuthContext,
);
export const fieldDescriptionSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.fieldDescriptions,
);
export const optionalFieldsSelector = createSelector(
commonComponentsSelector,
commonComponents => commonComponents.optionalFields,
);
export const tpaProvidersSelector = createSelector(
commonComponentsSelector,
commonComponents => ({
providers: commonComponents.thirdPartyAuthContext.providers,
secondaryProviders: commonComponents.thirdPartyAuthContext.secondaryProviders,
}),
);

View File

@@ -1,7 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const getThirdPartyAuthContext = async (urlParams : string) => {
// eslint-disable-next-line import/prefer-default-export
export async function getThirdPartyAuthContext(urlParams) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
params: urlParams,
@@ -12,14 +13,13 @@ const getThirdPartyAuthContext = async (urlParams : string) => {
.get(
`${getConfig().LMS_BASE_URL}/api/mfe_context`,
requestConfig,
);
)
.catch((e) => {
throw (e);
});
return {
fieldDescriptions: data.registrationFields || {},
optionalFields: data.optionalFields || {},
thirdPartyAuthContext: data.contextData || {},
};
};
export {
getThirdPartyAuthContext,
};
}

View File

@@ -0,0 +1,82 @@
import { PENDING_STATE } from '../../../data/constants';
import { THIRD_PARTY_AUTH_CONTEXT, THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG } from '../actions';
import reducer from '../reducers';
describe('common components reducer', () => {
it('test mfe context response', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: null,
},
};
const fieldDescriptions = {
fields: [],
};
const optionalFields = {
fields: [],
extended_profile: {},
};
const thirdPartyAuthContext = { ...state.thirdPartyAuthContext };
const action = {
type: THIRD_PARTY_AUTH_CONTEXT.SUCCESS,
payload: { fieldDescriptions, optionalFields, thirdPartyAuthContext },
};
expect(
reducer(state, action),
).toEqual(
{
...state,
fieldDescriptions: [],
optionalFields: {
fields: [],
extended_profile: {},
},
thirdPartyAuthApiStatus: 'complete',
},
);
});
it('should clear tpa context error message', () => {
const state = {
fieldDescriptions: {},
optionalFields: {},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [],
secondaryProviders: [],
pipelineUserDetails: null,
errorMessage: 'An error occurred',
},
};
const action = {
type: THIRD_PARTY_AUTH_CONTEXT_CLEAR_ERROR_MSG,
};
expect(
reducer(state, action),
).toEqual(
{
...state,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
...state.thirdPartyAuthContext,
errorMessage: null,
},
},
);
});
});

View File

@@ -0,0 +1,71 @@
import { runSaga } from 'redux-saga';
import { setCountryFromThirdPartyAuthContext } from '../../../register/data/actions';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { fetchThirdPartyAuthContext } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('fetchThirdPartyAuthContext', () => {
const params = {
payload: { urlParams: {} },
};
const data = {
currentProvider: null,
providers: [],
secondaryProviders: [],
finishAuthUrl: null,
pipelineUserDetails: {},
};
beforeEach(() => {
loggingService.logError.mockReset();
});
it('should call service and dispatch success action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.resolve({
thirdPartyAuthContext: data,
fieldDescriptions: {},
optionalFields: {},
}));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
setCountryFromThirdPartyAuthContext(),
actions.getThirdPartyAuthContextSuccess({}, {}, data),
]);
getThirdPartyAuthContext.mockClear();
});
it('should call service and dispatch error action', async () => {
const getThirdPartyAuthContext = jest.spyOn(api, 'getThirdPartyAuthContext')
.mockImplementation(() => Promise.reject(new Error('something went wrong')));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
fetchThirdPartyAuthContext,
params,
);
expect(getThirdPartyAuthContext).toHaveBeenCalledTimes(1);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.getThirdPartyAuthContextBegin(),
actions.getThirdPartyAuthContextFailure(),
]);
getThirdPartyAuthContext.mockClear();
});
});

View File

@@ -7,6 +7,9 @@ export { default as SocialAuthProviders } from './SocialAuthProviders';
export { default as ThirdPartyAuthAlert } from './ThirdPartyAuthAlert';
export { default as InstitutionLogistration } from './InstitutionLogistration';
export { RenderInstitutionButton } from './InstitutionLogistration';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';
export { storeName } from './data/selectors';
export { default as FormGroup } from './FormGroup';
export { default as PasswordField } from './PasswordField';
export { default as Zendesk } from './Zendesk';

View File

@@ -1,21 +1,15 @@
import React from 'react';
import { Provider } from 'react-redux';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { RegisterProvider } from '../../register/components/RegisterContext';
import { useFieldValidations } from '../../register/data/apiHook';
import { fetchRealtimeValidations } from '../../register/data/actions';
import FormGroup from '../FormGroup';
import PasswordField from '../PasswordField';
// Mock the useFieldValidations hook
jest.mock('../../register/data/apiHook', () => ({
useFieldValidations: jest.fn(),
}));
describe('FormGroup', () => {
const props = {
floatingLabel: 'Email',
@@ -41,52 +35,36 @@ describe('FormGroup', () => {
});
describe('PasswordField', () => {
const mockStore = configureStore();
let props = {};
let queryClient;
let mockMutate;
let store = {};
const renderWrapper = (children) => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<RegisterProvider>
{children}
</RegisterProvider>
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
register: {
validationApiRateLimited: false,
},
};
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
mockMutate = jest.fn();
useFieldValidations.mockReturnValue({
mutate: mockMutate,
isPending: false,
});
store = mockStore(initialState);
props = {
floatingLabel: 'Password',
name: 'password',
value: 'password123',
handleFocus: jest.fn(),
};
jest.clearAllMocks();
});
it('should show/hide password on icon click', () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
const showPasswordButton = getByLabelText('Show password');
@@ -99,7 +77,7 @@ describe('PasswordField', () => {
});
it('should show password requirement tooltip on focus', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -116,7 +94,7 @@ describe('PasswordField', () => {
...props,
value: '',
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -139,7 +117,7 @@ describe('PasswordField', () => {
});
it('should update password requirement checks', async () => {
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = getByLabelText('Password');
jest.useFakeTimers();
await act(async () => {
@@ -162,7 +140,7 @@ describe('PasswordField', () => {
});
it('should not run validations when blur is fired on password icon click', () => {
const { container, getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { container, getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
const passwordIcon = getByLabelText('Show password');
@@ -183,7 +161,7 @@ describe('PasswordField', () => {
...props,
handleBlur: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -201,7 +179,7 @@ describe('PasswordField', () => {
...props,
handleErrorChange: jest.fn(),
};
const { container } = render(renderWrapper(<PasswordField {...props} />));
const { container } = render(reduxWrapper(<PasswordField {...props} />));
const passwordInput = container.querySelector('input[name="password"]');
fireEvent.blur(passwordInput, {
@@ -224,7 +202,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -244,7 +222,7 @@ describe('PasswordField', () => {
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');
@@ -263,11 +241,12 @@ describe('PasswordField', () => {
});
it('should run backend validations when frontend validations pass on blur when rendered from register page', () => {
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
handleErrorChange: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordField = getByLabelText('Password');
fireEvent.blur(passwordField, {
target: {
@@ -276,17 +255,18 @@ describe('PasswordField', () => {
},
});
expect(mockMutate).toHaveBeenCalledWith({ password: 'password123' });
expect(store.dispatch).toHaveBeenCalledWith(fetchRealtimeValidations({ password: 'password123' }));
});
it('should use password value from prop when password icon is focused out (blur due to icon)', () => {
store.dispatch = jest.fn(store.dispatch);
props = {
...props,
value: 'testPassword',
handleErrorChange: jest.fn(),
handleBlur: jest.fn(),
};
const { getByLabelText } = render(renderWrapper(<PasswordField {...props} />));
const { getByLabelText } = render(reduxWrapper(<PasswordField {...props} />));
const passwordIcon = getByLabelText('Show password');

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';

View File

@@ -1,7 +1,9 @@
import React from 'react';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import renderer from 'react-test-renderer';
import { PENDING_STATE, REGISTER_PAGE } from '../../data/constants';
import { REGISTER_PAGE } from '../../data/constants';
import ThirdPartyAuthAlert from '../ThirdPartyAuthAlert';
describe('ThirdPartyAuthAlert', () => {
@@ -36,19 +38,4 @@ describe('ThirdPartyAuthAlert', () => {
).toJSON();
expect(tree).toMatchSnapshot();
});
it('renders skeleton for pending third-party auth', () => {
props = {
...props,
thirdPartyAuthApiStatus: PENDING_STATE,
isThirdPartyAuthActive: true,
};
const tree = renderer.create(
<IntlProvider locale="en">
<ThirdPartyAuthAlert {...props} />
</IntlProvider>,
).toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@@ -1,5 +1,7 @@
/* eslint-disable import/no-import-module-exports */
/* eslint-disable react/function-component-definition */
import React from 'react';
import { fetchAuthenticatedUser, getAuthenticatedUser } from '@edx/frontend-platform/auth';
import { render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';

View File

@@ -1,25 +1,5 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ThirdPartyAuthAlert renders skeleton for pending third-party auth 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"
id="tpa-alert"
role="alert"
>
<div
className="pgn__alert-message-wrapper"
>
<div
className="alert-message-content"
>
<p>
You have successfully signed into Google, but your Google account does not have a linked Your Platform Name Here account. To link your accounts, sign in now using your Your Platform Name Here password.
</p>
</div>
</div>
</div>
`;
exports[`ThirdPartyAuthAlert should match login page third party auth alert message snapshot 1`] = `
<div
className="fade alert-content alert-warning mt-n2 mb-5 alert show"

View File

@@ -1 +0,0 @@
export const appId = 'org.openedx.frontend.app.authn';

View File

@@ -0,0 +1,33 @@
import { getConfig } from '@edx/frontend-platform';
import { composeWithDevTools } from '@redux-devtools/extension';
import { applyMiddleware, compose, createStore } from 'redux';
import { createLogger } from 'redux-logger';
import createSagaMiddleware from 'redux-saga';
import thunkMiddleware from 'redux-thunk';
import createRootReducer from './reducers';
import rootSaga from './sagas';
const sagaMiddleware = createSagaMiddleware();
function composeMiddleware() {
if (getConfig().ENVIRONMENT === 'development') {
const loggerMiddleware = createLogger({
collapsed: true,
});
return composeWithDevTools(applyMiddleware(thunkMiddleware, sagaMiddleware, loggerMiddleware));
}
return compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
}
export default function configureStore(initialState = {}) {
const store = createStore(
createRootReducer(),
initialState,
composeMiddleware(),
);
sagaMiddleware.run(rootSaga);
return store;
}

36
src/data/reducers.js Executable file
View File

@@ -0,0 +1,36 @@
import { combineReducers } from 'redux';
import {
reducer as commonComponentsReducer,
storeName as commonComponentsStoreName,
} from '../common-components';
import {
reducer as forgotPasswordReducer,
storeName as forgotPasswordStoreName,
} from '../forgot-password';
import {
reducer as loginReducer,
storeName as loginStoreName,
} from '../login';
import {
reducer as authnProgressiveProfilingReducers,
storeName as authnProgressiveProfilingStoreName,
} from '../progressive-profiling';
import {
reducer as registerReducer,
storeName as registerStoreName,
} from '../register';
import {
reducer as resetPasswordReducer,
storeName as resetPasswordStoreName,
} from '../reset-password';
const createRootReducer = () => combineReducers({
[loginStoreName]: loginReducer,
[registerStoreName]: registerReducer,
[commonComponentsStoreName]: commonComponentsReducer,
[forgotPasswordStoreName]: forgotPasswordReducer,
[resetPasswordStoreName]: resetPasswordReducer,
[authnProgressiveProfilingStoreName]: authnProgressiveProfilingReducers,
});
export default createRootReducer;

19
src/data/sagas.js Normal file
View File

@@ -0,0 +1,19 @@
import { all } from 'redux-saga/effects';
import { saga as commonComponentsSaga } from '../common-components';
import { saga as forgotPasswordSaga } from '../forgot-password';
import { saga as loginSaga } from '../login';
import { saga as authnProgressiveProfilingSaga } from '../progressive-profiling';
import { saga as registrationSaga } from '../register';
import { saga as resetPasswordSaga } from '../reset-password';
export default function* rootSaga() {
yield all([
loginSaga(),
registrationSaga(),
commonComponentsSaga(),
forgotPasswordSaga(),
resetPasswordSaga(),
authnProgressiveProfilingSaga(),
]);
}

View File

@@ -0,0 +1,14 @@
import AsyncActionType from '../utils/reduxUtils';
describe('AsyncActionType', () => {
it('should return well formatted action strings', () => {
const actionType = new AsyncActionType('HOUSE_CATS', 'START_THE_RACE');
expect(actionType.BASE).toBe('HOUSE_CATS__START_THE_RACE');
expect(actionType.BEGIN).toBe('HOUSE_CATS__START_THE_RACE__BEGIN');
expect(actionType.SUCCESS).toBe('HOUSE_CATS__START_THE_RACE__SUCCESS');
expect(actionType.FAILURE).toBe('HOUSE_CATS__START_THE_RACE__FAILURE');
expect(actionType.RESET).toBe('HOUSE_CATS__START_THE_RACE__RESET');
expect(actionType.FORBIDDEN).toBe('HOUSE_CATS__START_THE_RACE__FORBIDDEN');
});
});

View File

@@ -7,4 +7,5 @@ export {
updatePathWithQueryParams,
windowScrollTo,
} from './dataUtils';
export { default as AsyncActionType } from './reduxUtils';
export { default as setCookie } from './cookies';

View File

@@ -0,0 +1,34 @@
/**
* Helper class to save time when writing out action types for asynchronous methods. Also helps
* ensure that actions are namespaced.
*/
export default class AsyncActionType {
constructor(topic, name) {
this.topic = topic;
this.name = name;
}
get BASE() {
return `${this.topic}__${this.name}`;
}
get BEGIN() {
return `${this.topic}__${this.name}__BEGIN`;
}
get SUCCESS() {
return `${this.topic}__${this.name}__SUCCESS`;
}
get FAILURE() {
return `${this.topic}__${this.name}__FAILURE`;
}
get RESET() {
return `${this.topic}__${this.name}__RESET`;
}
get FORBIDDEN() {
return `${this.topic}__${this.name}__FORBIDDEN`;
}
}

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { Form, Icon } from '@openedx/paragon';
import { ExpandMore } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { fireEvent, render } from '@testing-library/react';

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';
@@ -41,7 +43,7 @@ const ForgotPasswordAlert = (props) => {
}}
/>
);
break;
break;
case INTERNAL_SERVER_ERROR:
message = formatMessage(messages['internal.server.error']);
break;

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -12,39 +13,42 @@ import {
Tabs,
} from '@openedx/paragon';
import { ChevronLeft } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useLocation, useNavigate } from 'react-router-dom';
import { useNavigate } from 'react-router-dom';
import { useForgotPassword } from './data/apiHook';
import { forgotPassword, setForgotPasswordFormData } from './data/actions';
import { forgotPasswordResultSelector } from './data/selectors';
import ForgotPasswordAlert from './ForgotPasswordAlert';
import messages from './messages';
import BaseContainer from '../base-container';
import { FormGroup } from '../common-components';
import { LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { DEFAULT_STATE, LOGIN_PAGE, VALID_EMAIL_REGEX } from '../data/constants';
import { updatePathWithQueryParams, windowScrollTo } from '../data/utils';
const ForgotPasswordPage = () => {
const ForgotPasswordPage = (props) => {
const platformName = getConfig().SITE_NAME;
const emailRegex = new RegExp(VALID_EMAIL_REGEX, 'i');
const {
status, submitState, emailValidationError,
} = props;
const { formatMessage } = useIntl();
const navigate = useNavigate();
const location = useLocation();
const [email, setEmail] = useState('');
const [email, setEmail] = useState(props.email);
const [bannerEmail, setBannerEmail] = useState('');
const [formErrors, setFormErrors] = useState('');
const [validationError, setValidationError] = useState('');
const [status, setStatus] = useState(location.state?.status || null);
// React Query hook for forgot password
const { mutate: sendForgotPassword, isPending: isSending } = useForgotPassword();
const submitState = isSending ? 'pending' : 'default';
const [validationError, setValidationError] = useState(emailValidationError);
const navigate = useNavigate();
useEffect(() => {
sendPageEvent('login_and_registration', 'reset');
sendTrackEvent('edx.bi.password_reset_form.viewed', { category: 'user-engagement' });
}, []);
useEffect(() => {
setValidationError(emailValidationError);
}, [emailValidationError]);
useEffect(() => {
if (status === 'complete') {
setEmail('');
@@ -64,38 +68,22 @@ const ForgotPasswordPage = () => {
};
const handleBlur = () => {
setValidationError(getValidationMessage(email));
props.setForgotPasswordFormData({ email, emailValidationError: getValidationMessage(email) });
};
const handleFocus = () => {
setValidationError('');
};
const handleFocus = () => props.setForgotPasswordFormData({ emailValidationError: '' });
const handleSubmit = (e) => {
e.preventDefault();
setBannerEmail(email);
const validateError = getValidationMessage(email);
if (validateError) {
setFormErrors(validateError);
setValidationError(validateError);
const error = getValidationMessage(email);
if (error) {
setFormErrors(error);
props.setForgotPasswordFormData({ email, emailValidationError: error });
windowScrollTo({ left: 0, top: 0, behavior: 'smooth' });
} else {
setFormErrors('');
sendForgotPassword(email, {
onSuccess: (data, emailUsed) => {
setStatus('complete');
setBannerEmail(emailUsed);
setFormErrors('');
},
onError: (error) => {
if (error.response && error.response.status === 403) {
setStatus('forbidden');
} else {
setStatus('server-error');
}
},
});
props.forgotPassword(email);
}
};
@@ -165,7 +153,7 @@ const ForgotPasswordPage = () => {
)}
<p className="mt-5.5 small text-gray-700">
{formatMessage(messages['additional.help.text'], { platformName })}
<span className="mx-1">
<span>
<Hyperlink isInline destination={`mailto:${getConfig().INFO_EMAIL}`}>{getConfig().INFO_EMAIL}</Hyperlink>
</span>
</p>
@@ -176,4 +164,26 @@ const ForgotPasswordPage = () => {
);
};
export default ForgotPasswordPage;
ForgotPasswordPage.propTypes = {
email: PropTypes.string,
emailValidationError: PropTypes.string,
forgotPassword: PropTypes.func.isRequired,
setForgotPasswordFormData: PropTypes.func.isRequired,
status: PropTypes.string,
submitState: PropTypes.string,
};
ForgotPasswordPage.defaultProps = {
email: '',
emailValidationError: '',
status: null,
submitState: DEFAULT_STATE,
};
export default connect(
forgotPasswordResultSelector,
{
forgotPassword,
setForgotPasswordFormData,
},
)(ForgotPasswordPage);

View File

@@ -0,0 +1,32 @@
import { AsyncActionType } from '../../data/utils';
export const FORGOT_PASSWORD = new AsyncActionType('FORGOT', 'PASSWORD');
export const FORGOT_PASSWORD_PERSIST_FORM_DATA = 'FORGOT_PASSWORD_PERSIST_FORM_DATA';
// Forgot Password
export const forgotPassword = email => ({
type: FORGOT_PASSWORD.BASE,
payload: { email },
});
export const forgotPasswordBegin = () => ({
type: FORGOT_PASSWORD.BEGIN,
});
export const forgotPasswordSuccess = email => ({
type: FORGOT_PASSWORD.SUCCESS,
payload: { email },
});
export const forgotPasswordForbidden = () => ({
type: FORGOT_PASSWORD.FORBIDDEN,
});
export const forgotPasswordServerError = () => ({
type: FORGOT_PASSWORD.FAILURE,
});
export const setForgotPasswordFormData = (forgotPasswordFormData) => ({
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
});

View File

@@ -1,144 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
import { forgotPassword } from './api';
// Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('form-urlencoded', () => jest.fn());
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockFormurlencoded = formurlencoded as jest.MockedFunction<typeof formurlencoded>;
describe('forgot-password api', () => {
const mockHttpClient = {
post: jest.fn(),
};
const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000',
};
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockFormurlencoded.mockImplementation((data) => `encoded=${JSON.stringify(data)}`);
});
describe('forgotPassword', () => {
const testEmail = 'test@example.com';
const expectedUrl = `${mockConfig.LMS_BASE_URL}/account/password`;
const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
it('should send forgot password request successfully', async () => {
const mockResponse = {
data: {
message: 'Password reset email sent successfully',
success: true,
},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: testEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: testEmail })}`,
expectedConfig
);
expect(result).toEqual(mockResponse.data);
});
it('should handle empty email address', async () => {
const emptyEmail = '';
const mockResponse = {
data: {
message: 'Email is required',
success: false,
}
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(emptyEmail);
expect(mockFormurlencoded).toHaveBeenCalledWith({ email: emptyEmail });
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`encoded=${JSON.stringify({ email: emptyEmail })}`,
expectedConfig,
);
expect(result).toEqual(mockResponse.data);
});
it('should handle network errors without response', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockHttpClient.post.mockRejectedValueOnce(networkError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Network Error');
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
expect.any(String),
expectedConfig
);
});
it('should handle timeout errors', async () => {
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
mockHttpClient.post.mockRejectedValueOnce(timeoutError);
await expect(forgotPassword(testEmail)).rejects.toThrow('Request timeout');
});
it('should handle response with no data field', async () => {
const mockResponse = {
// No data field
status: 200,
statusText: 'OK',
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toBeUndefined();
});
it('should return exactly the data field from response', async () => {
const expectedData = {
message: 'Password reset email sent successfully',
success: true,
timestamp: '2026-02-05T10:00:00Z',
};
const mockResponse = {
data: expectedData,
status: 200,
headers: {},
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
const result = await forgotPassword(testEmail);
expect(result).toEqual(expectedData);
expect(result).not.toHaveProperty('status');
expect(result).not.toHaveProperty('headers');
});
});
});

View File

@@ -1,175 +0,0 @@
import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import * as api from './api';
import { useForgotPassword } from './apiHook';
// Mock the logging functions
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
// Mock the API function
jest.mock('./api', () => ({
forgotPassword: jest.fn(),
}));
const mockForgotPassword = api.forgotPassword as jest.MockedFunction<typeof api.forgotPassword>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockLogInfo = logInfo as jest.MockedFunction<typeof logInfo>;
// Test wrapper component
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function TestWrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useForgotPassword', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isSuccess).toBe(false);
expect(result.current.error).toBe(null);
});
it('should send forgot password email successfully and log success', async () => {
const testEmail = 'test@example.com';
const mockResponse = {
message: 'Password reset email sent successfully',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
it('should handle 403 forbidden error and log as info', async () => {
const testEmail = 'blocked@example.com';
const mockError = {
response: {
status: 403,
data: {
detail: 'Too many password reset attempts',
},
},
message: 'Forbidden',
};
mockForgotPassword.mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogInfo).toHaveBeenCalledWith(mockError);
expect(mockLogError).not.toHaveBeenCalled();
expect(result.current.error).toEqual(mockError);
});
it('should handle network errors without response and log as error', async () => {
const testEmail = 'test@example.com';
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockForgotPassword.mockRejectedValueOnce(networkError);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(mockLogError).toHaveBeenCalledWith(networkError);
expect(mockLogInfo).not.toHaveBeenCalled();
expect(result.current.error).toEqual(networkError);
});
it('should handle empty email address', async () => {
const testEmail = '';
const mockResponse = {
message: 'Email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith('');
});
it('should handle email with special characters', async () => {
const testEmail = 'user+test@example-domain.co.uk';
const mockResponse = {
message: 'Password reset email sent',
success: true,
};
mockForgotPassword.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useForgotPassword(), {
wrapper: createWrapper(),
});
result.current.mutate(testEmail);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockForgotPassword).toHaveBeenCalledWith(testEmail);
expect(result.current.data).toEqual(mockResponse);
});
});

View File

@@ -1,47 +0,0 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { useMutation } from '@tanstack/react-query';
import { forgotPassword } from './api';
interface ForgotPasswordResult {
success: boolean;
message?: string;
}
interface UseForgotPasswordOptions {
onSuccess?: (data: ForgotPasswordResult, email: string) => void;
onError?: (error: Error) => void;
}
interface ApiError {
response?: {
status: number;
data: Record<string, unknown>;
};
}
const useForgotPassword = (options: UseForgotPasswordOptions = {}) => useMutation({
mutationFn: (email: string) => (
forgotPassword(email)
),
onSuccess: (data: ForgotPasswordResult, email: string) => {
if (options.onSuccess) {
options.onSuccess(data, email);
}
},
onError: (error: ApiError) => {
// Handle different error types like the saga did
if (error.response && error.response.status === 403) {
logInfo(error);
} else {
logError(error);
}
if (options.onError) {
options.onError(error as Error);
}
},
});
export {
useForgotPassword,
};

View File

@@ -0,0 +1,58 @@
import { FORGOT_PASSWORD, FORGOT_PASSWORD_PERSIST_FORM_DATA } from './actions';
import { INTERNAL_SERVER_ERROR, PENDING_STATE } from '../../data/constants';
import { PASSWORD_RESET_FAILURE } from '../../reset-password/data/actions';
export const defaultState = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const reducer = (state = defaultState, action = null) => {
if (action !== null) {
switch (action.type) {
case FORGOT_PASSWORD.BEGIN:
return {
email: state.email,
status: 'pending',
submitState: PENDING_STATE,
};
case FORGOT_PASSWORD.SUCCESS:
return {
...defaultState,
status: 'complete',
};
case FORGOT_PASSWORD.FORBIDDEN:
return {
email: state.email,
status: 'forbidden',
};
case FORGOT_PASSWORD.FAILURE:
return {
email: state.email,
status: INTERNAL_SERVER_ERROR,
};
case PASSWORD_RESET_FAILURE:
return {
status: action.payload.errorCode,
};
case FORGOT_PASSWORD_PERSIST_FORM_DATA: {
const { forgotPasswordFormData } = action.payload;
return {
...state,
...forgotPasswordFormData,
};
}
default:
return {
...defaultState,
email: state.email,
emailValidationError: state.emailValidationError,
};
}
}
return state;
};
export default reducer;

View File

@@ -0,0 +1,35 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
// Actions
import {
FORGOT_PASSWORD,
forgotPasswordBegin,
forgotPasswordForbidden,
forgotPasswordServerError,
forgotPasswordSuccess,
} from './actions';
import { forgotPassword } from './service';
// Services
export function* handleForgotPassword(action) {
try {
yield put(forgotPasswordBegin());
yield call(forgotPassword, action.payload.email);
yield put(forgotPasswordSuccess(action.payload.email));
} catch (e) {
if (e.response && e.response.status === 403) {
yield put(forgotPasswordForbidden());
logInfo(e);
} else {
yield put(forgotPasswordServerError());
logError(e);
}
}
}
export default function* saga() {
yield takeEvery(FORGOT_PASSWORD.BASE, handleForgotPassword);
}

View File

@@ -0,0 +1,10 @@
import { createSelector } from 'reselect';
export const storeName = 'forgotPassword';
export const forgotPasswordSelector = state => ({ ...state[storeName] });
export const forgotPasswordResultSelector = createSelector(
forgotPasswordSelector,
forgotPassword => forgotPassword,
);

View File

@@ -2,7 +2,8 @@ import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import formurlencoded from 'form-urlencoded';
const forgotPassword = async (email: string) => {
// eslint-disable-next-line import/prefer-default-export
export async function forgotPassword(email) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
@@ -19,8 +20,4 @@ const forgotPassword = async (email: string) => {
});
return data;
};
export {
forgotPassword,
};
}

View File

@@ -0,0 +1,34 @@
import {
FORGOT_PASSWORD_PERSIST_FORM_DATA,
} from '../actions';
import reducer from '../reducers';
describe('forgot password reducer', () => {
it('should set email and emailValidationError', () => {
const state = {
status: '',
submitState: '',
email: '',
emailValidationError: '',
};
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
const action = {
type: FORGOT_PASSWORD_PERSIST_FORM_DATA,
payload: { forgotPasswordFormData },
};
expect(
reducer(state, action),
).toEqual(
{
status: '',
submitState: '',
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
},
);
});
});

View File

@@ -0,0 +1,67 @@
import { runSaga } from 'redux-saga';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { handleForgotPassword } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('handleForgotPassword', () => {
const params = {
payload: {
forgotPasswordFormData: {
email: 'test@test.com',
},
},
};
beforeEach(() => {
loggingService.logError.mockReset();
loggingService.logInfo.mockReset();
});
it('should handle 500 error code', async () => {
const passwordErrorResponse = { response: { status: 500 } };
const forgotPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
() => Promise.reject(passwordErrorResponse),
);
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleForgotPassword,
params,
);
expect(loggingService.logError).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.forgotPasswordBegin(),
actions.forgotPasswordServerError(),
]);
forgotPasswordRequest.mockClear();
});
it('should handle rate limit error', async () => {
const forbiddenErrorResponse = { response: { status: 403 } };
const forbiddenPasswordRequest = jest.spyOn(api, 'forgotPassword').mockImplementation(
() => Promise.reject(forbiddenErrorResponse),
);
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleForgotPassword,
params,
);
expect(loggingService.logInfo).toHaveBeenCalled();
expect(dispatched).toEqual([
actions.forgotPasswordBegin(),
actions.forgotPasswordForbidden(null),
]);
forbiddenPasswordRequest.mockClear();
});
});

View File

@@ -1 +1,5 @@
export { default as ForgotPasswordPage } from './ForgotPasswordPage';
export { default as reducer } from './data/reducers';
export { FORGOT_PASSWORD } from './data/actions';
export { default as saga } from './data/sagas';
export { storeName, forgotPasswordResultSelector } from './data/selectors';

View File

@@ -74,7 +74,7 @@ const messages = defineMessages({
},
'additional.help.text': {
id: 'additional.help.text',
defaultMessage: 'For additional help, contact {platformName} support at',
defaultMessage: 'For additional help, contact {platformName} support at ',
description: 'additional help text on forgot password page',
},
'sign.in.text': {

View File

@@ -1,17 +1,16 @@
import { Provider } from 'react-redux';
import { mergeConfig } from '@edx/frontend-platform';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
fireEvent, render, screen, waitFor,
fireEvent, render, screen,
} from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import {
FORBIDDEN_STATE, FORM_SUBMISSION_ERROR, INTERNAL_SERVER_ERROR, LOGIN_PAGE,
} from '../../data/constants';
import { INTERNAL_SERVER_ERROR, LOGIN_PAGE } from '../../data/constants';
import { PASSWORD_RESET } from '../../reset-password/data/constants';
import { useForgotPassword } from '../data/apiHook';
import ForgotPasswordAlert from '../ForgotPasswordAlert';
import { setForgotPasswordFormData } from '../data/actions';
import ForgotPasswordPage from '../ForgotPasswordPage';
const mockedNavigator = jest.fn();
@@ -26,9 +25,13 @@ jest.mock('react-router-dom', () => ({
useNavigate: () => mockedNavigator,
}));
jest.mock('../data/apiHook', () => ({
useForgotPassword: jest.fn(),
}));
const mockStore = configureStore();
const initialState = {
forgotPassword: {
status: '',
},
};
describe('ForgotPasswordPage', () => {
mergeConfig({
@@ -36,55 +39,19 @@ describe('ForgotPasswordPage', () => {
INFO_EMAIL: '',
});
let queryClient;
let mockMutate;
let mockIsPending;
let props = {};
let store = {};
const renderWrapper = (component, options = {}) => {
const {
status = null,
isPending = false,
mutateImplementation = jest.fn(),
} = options;
mockMutate = jest.fn((email, callbacks) => {
if (mutateImplementation && typeof mutateImplementation === 'function') {
mutateImplementation(email, callbacks);
}
});
mockIsPending = isPending;
useForgotPassword.mockReturnValue({
mutate: mockMutate,
isPending: mockIsPending,
isError: status === 'error' || status === 'server-error',
isSuccess: status === 'complete',
});
return (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
{component}
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
);
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
beforeEach(() => {
// Create a fresh QueryClient for each test
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
},
mutations: {
retry: false,
},
},
});
store = mockStore(initialState);
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
@@ -99,13 +66,17 @@ describe('ForgotPasswordPage', () => {
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
// Clear mock calls between tests
jest.clearAllMocks();
props = {
forgotPassword: jest.fn(),
status: null,
};
});
const findByTextContent = (container, text) => Array.from(container.querySelectorAll('*')).find(
element => element.textContent === text,
);
it('not should display need other help signing in button', () => {
const { queryByTestId } = render(renderWrapper(<ForgotPasswordPage />));
const { queryByTestId } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const forgotPasswordButton = queryByTestId('forgot-password');
expect(forgotPasswordButton).toBeNull();
});
@@ -114,14 +85,14 @@ describe('ForgotPasswordPage', () => {
mergeConfig({
LOGIN_ISSUE_SUPPORT_LINK: '/support',
});
render(renderWrapper(<ForgotPasswordPage />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const forgotPasswordButton = screen.findByText('Need help signing in?');
expect(forgotPasswordButton).toBeDefined();
});
it('should display email validation error message', async () => {
const validationMessage = 'We were unable to contact you.Enter a valid email address below.';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -135,28 +106,23 @@ describe('ForgotPasswordPage', () => {
expect(validationErrors).toBe(validationMessage);
});
it('should show alert on server error', async () => {
it('should show alert on server error', () => {
store = mockStore({
forgotPassword: { status: INTERNAL_SERVER_ERROR },
});
const expectedMessage = 'We were unable to contact you.'
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
// Create a component with server-error status to simulate the error state
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'server-error',
}));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
// The ForgotPasswordAlert should render with server error status
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(expectedMessage);
}
});
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(expectedMessage);
});
it('should display empty email validation message', () => {
it('should display empty email validation message', async () => {
const validationMessage = 'We were unable to contact you.Enter your email below.';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
@@ -167,25 +133,21 @@ describe('ForgotPasswordPage', () => {
expect(validationErrors).toBe(validationMessage);
});
it('should display request in progress error message', async () => {
it('should display request in progress error message', () => {
const rateLimitMessage = 'An error occurred.Your previous request is in progress, please try again in a few moments.';
// Create component with forbidden status to simulate rate limit error
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'forbidden',
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(rateLimitMessage);
}
store = mockStore({
forgotPassword: { status: 'forbidden' },
});
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const alertElements = container.querySelectorAll('.alert-danger');
const validationErrors = alertElements[0].textContent;
expect(validationErrors).toBe(rateLimitMessage);
});
it('should not display any error message on change event', () => {
render(renderWrapper(<ForgotPasswordPage />));
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
@@ -195,248 +157,115 @@ describe('ForgotPasswordPage', () => {
expect(errorElement).toBeNull();
});
it('should not cause errors when blur event occurs', () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should set error in redux store on onBlur', () => {
const forgotPasswordFormData = {
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: '',
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
// Simply test that blur event doesn't cause errors
fireEvent.blur(emailInput);
// No error assertions needed as we're just testing stability
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
it('should display validation error message when invalid email is submitted', () => {
it('should display error message if available in props', async () => {
const validationMessage = 'Enter your email';
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const submitButton = screen.getByText('Submit');
fireEvent.click(submitButton);
props = {
...props,
emailValidationError: validationMessage,
email: '',
};
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const validationElement = container.querySelector('.pgn__form-text-invalid');
expect(validationElement.textContent).toEqual(validationMessage);
});
it('should not cause errors when focus event occurs', () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should clear error in redux store on onFocus', () => {
const forgotPasswordFormData = {
emailValidationError: '',
};
props = {
...props,
email: 'test@gmail',
emailValidationError: 'Enter a valid email address',
};
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const emailInput = screen.getByLabelText('Email');
fireEvent.focus(emailInput);
expect(store.dispatch).toHaveBeenCalledWith(setForgotPasswordFormData(forgotPasswordFormData));
});
it('should not display error message initially', async () => {
render(renderWrapper(<ForgotPasswordPage />));
it('should clear error message when cleared in props on focus', async () => {
props = {
...props,
emailValidationError: '',
email: '',
};
render(reduxWrapper(<ForgotPasswordPage {...props} />));
const errorElement = screen.queryByTestId('email-invalid-feedback');
expect(errorElement).toBeNull();
});
it('should display success message after email is sent', async () => {
const testEmail = 'test@example.com';
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: 'complete',
}));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: testEmail } });
fireEvent.click(submitButton);
await waitFor(() => {
const successElements = container.querySelectorAll('.alert-success');
if (successElements.length > 0) {
const successMessage = successElements[0].textContent;
expect(successMessage).toContain('Check your email');
expect(successMessage).toContain('We sent an email');
}
it('should display success message after email is sent', () => {
store = mockStore({
...initialState,
forgotPassword: {
status: 'complete',
},
});
const successMessage = 'Check your emailWe sent an email to with instructions to reset your password. If you do not '
+ 'receive a password reset message after 1 minute, verify that you entered the correct email address,'
+ ' or check your spam folder. If you need further assistance, contact technical support.';
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
expect(successElement).toBeDefined();
expect(successElement.textContent).toEqual(successMessage);
});
it('should call mutation on form submission with valid email', async () => {
render(renderWrapper(<ForgotPasswordPage />));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
// Verify the mutation was called with the correct email and callbacks
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith('test@example.com', expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}));
it('should display invalid password reset link error', () => {
store = mockStore({
...initialState,
forgotPassword: {
status: PASSWORD_RESET.INVALID_TOKEN,
},
});
});
const successMessage = 'Invalid password reset link'
+ 'This password reset link is invalid. It may have been used already. '
+ 'Enter your email below to receive a new link.';
it('should call mutation with success callback', async () => {
const successMutation = (email, { onSuccess }) => {
onSuccess({}, email);
};
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const successElement = findByTextContent(container, successMessage);
render(renderWrapper(<ForgotPasswordPage />, {
mutateImplementation: successMutation,
}));
const emailInput = screen.getByLabelText('Email');
const submitButton = screen.getByText('Submit');
fireEvent.change(emailInput, { target: { value: 'test@example.com' } });
fireEvent.click(submitButton);
await waitFor(() => {
expect(mockMutate).toHaveBeenCalledWith('test@example.com', expect.objectContaining({
onSuccess: expect.any(Function),
onError: expect.any(Function),
}));
});
expect(successElement).toBeDefined();
expect(successElement.textContent).toEqual(successMessage);
});
it('should redirect onto login page', async () => {
const { container } = render(renderWrapper(<ForgotPasswordPage />));
const { container } = render(reduxWrapper(<ForgotPasswordPage {...props} />));
const navElement = container.querySelector('nav');
const anchorElement = navElement.querySelector('a');
fireEvent.click(anchorElement);
expect(mockedNavigator).toHaveBeenCalledWith(expect.stringContaining(LOGIN_PAGE));
});
it('should display token validation rate limit error message', async () => {
const expectedHeading = 'Too many requests';
const expectedMessage = 'An error has occurred because of too many requests. Please try again after some time.';
const { container } = render(renderWrapper(<ForgotPasswordPage />, {
status: PASSWORD_RESET.FORBIDDEN_REQUEST,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
it('should display invalid token error message', async () => {
const expectedHeading = 'Invalid password reset link';
const expectedMessage = 'This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.';
const { container } = render(renderWrapper(<ForgotPasswordAlert />, {
status: PASSWORD_RESET.INVALID_TOKEN,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
it('should display token validation internal server error message', async () => {
const expectedHeading = 'Token validation failure';
const expectedMessage = 'An error has occurred. Try refreshing the page, or check your internet connection.';
const { container } = render(renderWrapper(<ForgotPasswordAlert />, {
status: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
}));
await waitFor(() => {
const alertElements = container.querySelectorAll('.alert-danger');
if (alertElements.length > 0) {
const alertContent = alertElements[0].textContent;
expect(alertContent).toContain(expectedHeading);
expect(alertContent).toContain(expectedMessage);
}
});
});
});
describe('ForgotPasswordAlert', () => {
const renderAlertWrapper = (props) => {
const queryClient = new QueryClient();
return render(
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<ForgotPasswordAlert {...props} />
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>,
);
};
it('should display internal server error message', () => {
const { container } = renderAlertWrapper({
status: INTERNAL_SERVER_ERROR,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('We were unable to contact you.');
expect(alertElement.textContent).toContain('An error has occurred. Try refreshing the page, or check your internet connection.');
});
it('should display forbidden state error message', () => {
const { container } = renderAlertWrapper({
status: FORBIDDEN_STATE,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('An error occurred.');
expect(alertElement.textContent).toContain('Your previous request is in progress, please try again in a few moments.');
});
it('should display form submission error message', () => {
const emailError = 'Enter a valid email address';
const { container } = renderAlertWrapper({
status: FORM_SUBMISSION_ERROR,
email: 'test@example.com',
emailError,
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('We were unable to contact you.');
expect(alertElement.textContent).toContain(`${emailError} below.`);
});
it('should display password reset invalid token error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.INVALID_TOKEN,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Invalid password reset link');
expect(alertElement.textContent).toContain('This password reset link is invalid. It may have been used already. Enter your email below to receive a new link.');
});
it('should display password reset forbidden request error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.FORBIDDEN_REQUEST,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Too many requests');
expect(alertElement.textContent).toContain('An error has occurred because of too many requests. Please try again after some time.');
});
it('should display password reset internal server error message', () => {
const { container } = renderAlertWrapper({
status: PASSWORD_RESET.INTERNAL_SERVER_ERROR,
email: 'test@example.com',
emailError: '',
});
const alertElement = container.querySelector('.alert-danger');
expect(alertElement).toBeTruthy();
expect(alertElement.textContent).toContain('Token validation failure');
expect(alertElement.textContent).toContain('An error has occurred. Try refreshing the page, or check your internet connection.');
expect(mockedNavigator).toHaveBeenCalledWith(LOGIN_PAGE);
});
});

View File

@@ -1,7 +1,7 @@
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import { StrictMode } from 'react';
import React, { StrictMode } from 'react';
import {
APP_INIT_ERROR, APP_READY, initialize, mergeConfig, subscribe,

View File

@@ -1,3 +1,5 @@
import React from 'react';
import { getConfig } from '@edx/frontend-platform';
import { FormattedMessage, useIntl } from '@edx/frontend-platform/i18n';
import { Alert } from '@openedx/paragon';

View File

@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { useIntl } from '@edx/frontend-platform/i18n';
@@ -26,7 +26,7 @@ const ChangePasswordPrompt = ({ variant, redirectUrl }) => {
}
},
};
// eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
// eslint-disable-next-line no-unused-vars
const [isOpen, open, close] = useToggle(true, handlers);
const { formatMessage } = useIntl();
const navigate = useNavigate();

View File

@@ -1,4 +1,4 @@
import { useEffect } from 'react';
import React, { useEffect } from 'react';
import { getConfig } from '@edx/frontend-platform';
import { getAuthService } from '@edx/frontend-platform/auth';

View File

@@ -1,14 +1,26 @@
import { useEffect, useMemo, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Form, StatefulButton } from '@openedx/paragon';
import {
Form, StatefulButton,
} from '@openedx/paragon';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import Skeleton from 'react-loading-skeleton';
import { Link, useLocation } from 'react-router-dom';
import { Link } from 'react-router-dom';
import AccountActivationMessage from './AccountActivationMessage';
import {
backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
} from './data/actions';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
import {
FormGroup,
InstitutionLogistration,
@@ -16,12 +28,13 @@ import {
RedirectLogistration,
ThirdPartyAuthAlert,
} from '../common-components';
import AccountActivationMessage from './AccountActivationMessage';
import { useThirdPartyAuthContext } from '../common-components/components/ThirdPartyAuthContext';
import { useThirdPartyAuthHook } from '../common-components/data/apiHook';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import { thirdPartyAuthContextSelector } from '../common-components/data/selectors';
import EnterpriseSSO from '../common-components/EnterpriseSSO';
import ThirdPartyAuth from '../common-components/ThirdPartyAuth';
import { LOGIN_PAGE, PENDING_STATE, RESET_PAGE } from '../data/constants';
import {
DEFAULT_STATE, PENDING_STATE, RESET_PAGE,
} from '../data/constants';
import {
getActivationStatus,
getAllPossibleQueryParams,
@@ -30,93 +43,72 @@ import {
updatePathWithQueryParams,
} from '../data/utils';
import ResetPasswordSuccess from '../reset-password/ResetPasswordSuccess';
import { useLoginContext } from './components/LoginContext';
import { useLogin } from './data/apiHook';
import { INVALID_FORM, TPA_AUTHENTICATION_FAILURE } from './data/constants';
import LoginFailureMessage from './LoginFailure';
import messages from './messages';
const LoginPage = ({
institutionLogin,
handleInstitutionLogin,
}) => {
// Context for third-party auth
const LoginPage = (props) => {
const {
backedUpFormData,
loginErrorCode,
loginErrorContext,
loginResult,
shouldBackupState,
thirdPartyAuthContext: {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
},
thirdPartyAuthApiStatus,
thirdPartyAuthContext,
setThirdPartyAuthContextBegin,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
} = useThirdPartyAuthContext();
const location = useLocation();
const {
formFields,
setFormFields,
errors,
setErrors,
} = useLoginContext();
// React Query for server state
const [loginResult, setLoginResult] = useState({ success: false, redirectUrl: '' });
const [errorCode, setErrorCode] = useState({
type: '',
count: 0,
context: {},
});
const { mutate: loginUser, isPending: isLoggingIn } = useLogin({
onSuccess: (data) => {
setLoginResult({ success: true, redirectUrl: data.redirectUrl || '' });
},
onError: (formattedError) => {
setErrorCode(prev => ({
type: formattedError.type,
count: prev.count + 1,
context: formattedError.context,
}));
},
});
const [showResetPasswordSuccessBanner,
setShowResetPasswordSuccessBanner] = useState(location.state?.showResetPasswordSuccessBanner || null);
const {
providers,
currentProvider,
secondaryProviders,
finishAuthUrl,
platformName,
errorMessage: thirdPartyErrorMessage,
} = thirdPartyAuthContext;
institutionLogin,
showResetPasswordSuccessBanner,
submitState,
// Actions
backupFormState,
handleInstitutionLogin,
getTPADataFromBackend,
} = props;
const { formatMessage } = useIntl();
const activationMsgType = getActivationStatus();
const queryParams = useMemo(() => getAllPossibleQueryParams(), []);
const tpaHint = useMemo(() => getTpaHint(), []);
const params = { ...queryParams };
if (tpaHint) {
params.tpa_hint = tpaHint;
}
const { data, isSuccess, error } = useThirdPartyAuthHook(LOGIN_PAGE, params);
const [formFields, setFormFields] = useState({ ...backedUpFormData.formFields });
const [errorCode, setErrorCode] = useState({ type: '', count: 0, context: {} });
const [errors, setErrors] = useState({ ...backedUpFormData.errors });
const tpaHint = getTpaHint();
useEffect(() => {
sendPageEvent('login_and_registration', 'login');
}, []);
// Fetch third-party auth context data
useEffect(() => {
setThirdPartyAuthContextBegin();
if (isSuccess && data) {
setThirdPartyAuthContextSuccess(
data.fieldDescriptions,
data.optionalFields,
data.thirdPartyAuthContext,
);
const payload = { ...queryParams };
if (tpaHint) {
payload.tpa_hint = tpaHint;
}
if (error) {
setThirdPartyAuthContextFailure();
getTPADataFromBackend(payload);
}, [getTPADataFromBackend, queryParams, tpaHint]);
/**
* Backup the login form in redux when login page is toggled.
*/
useEffect(() => {
if (shouldBackupState) {
backupFormState({
formFields: { ...formFields },
errors: { ...errors },
});
}
}, [tpaHint, queryParams, isSuccess, data, error,
setThirdPartyAuthContextBegin, setThirdPartyAuthContextSuccess, setThirdPartyAuthContextFailure]);
}, [shouldBackupState, formFields, errors, backupFormState]);
useEffect(() => {
if (loginErrorCode) {
setErrorCode(prevState => ({
type: loginErrorCode,
count: prevState.count + 1,
context: { ...loginErrorContext },
}));
}
}, [loginErrorCode, loginErrorContext]);
useEffect(() => {
if (thirdPartyErrorMessage) {
@@ -131,10 +123,7 @@ const LoginPage = ({
}, [thirdPartyErrorMessage]);
const validateFormFields = (payload) => {
const {
emailOrUsername,
password,
} = payload;
const { emailOrUsername, password } = payload;
const fieldErrors = { ...errors };
if (emailOrUsername === '') {
@@ -152,18 +141,14 @@ const LoginPage = ({
const handleSubmit = (event) => {
event.preventDefault();
if (showResetPasswordSuccessBanner) {
setShowResetPasswordSuccessBanner(false);
props.dismissPasswordResetBanner();
}
const formData = { ...formFields };
const validationErrors = validateFormFields(formData);
if (validationErrors.emailOrUsername || validationErrors.password) {
setErrors(validationErrors);
setErrorCode(prev => ({
type: INVALID_FORM,
count: prev.count + 1,
context: {},
}));
setErrors({ ...validationErrors });
setErrorCode(prevState => ({ type: INVALID_FORM, count: prevState.count + 1, context: {} }));
return;
}
@@ -173,36 +158,23 @@ const LoginPage = ({
password: formData.password,
...queryParams,
};
loginUser(payload);
props.loginRequest(payload);
};
const handleOnChange = (event) => {
const {
name,
value,
} = event.target;
// Save to context for persistence across tab switches
setFormFields(prevState => ({
...prevState,
[name]: value,
}));
const { name, value } = event.target;
setFormFields(prevState => ({ ...prevState, [name]: value }));
};
const handleOnFocus = (event) => {
const { name } = event.target;
setErrors(prevErrors => ({
...prevErrors,
[name]: '',
}));
setErrors(prevErrors => ({ ...prevErrors, [name]: '' }));
};
const trackForgotPasswordLinkClick = () => {
sendTrackEvent('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
};
const {
provider,
skipHintedLogin,
} = getTpaProvider(tpaHint, providers, secondaryProviders);
const { provider, skipHintedLogin } = getTpaProvider(tpaHint, providers, secondaryProviders);
if (tpaHint) {
if (thirdPartyAuthApiStatus === PENDING_STATE) {
@@ -227,7 +199,6 @@ const LoginPage = ({
/>
);
}
return (
<>
<Helmet>
@@ -279,10 +250,10 @@ const LoginPage = ({
type="submit"
variant="brand"
className="login-button-width"
state={(isLoggingIn ? PENDING_STATE : 'default')}
state={submitState}
labels={{
default: formatMessage(messages['sign.in.button']),
pending: 'pending',
pending: '',
}}
onClick={handleSubmit}
onMouseDown={(event) => event.preventDefault()}
@@ -310,9 +281,88 @@ const LoginPage = ({
);
};
const mapStateToProps = state => {
const loginPageState = state.login;
return {
backedUpFormData: loginPageState.loginFormData,
loginErrorCode: loginPageState.loginErrorCode,
loginErrorContext: loginPageState.loginErrorContext,
loginResult: loginPageState.loginResult,
shouldBackupState: loginPageState.shouldBackupState,
showResetPasswordSuccessBanner: loginPageState.showResetPasswordSuccessBanner,
submitState: loginPageState.submitState,
thirdPartyAuthContext: thirdPartyAuthContextSelector(state),
thirdPartyAuthApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
};
};
LoginPage.propTypes = {
backedUpFormData: PropTypes.shape({
formFields: PropTypes.shape({}),
errors: PropTypes.shape({}),
}),
loginErrorCode: PropTypes.string,
loginErrorContext: PropTypes.shape({
email: PropTypes.string,
redirectUrl: PropTypes.string,
context: PropTypes.shape({}),
}),
loginResult: PropTypes.shape({
redirectUrl: PropTypes.string,
success: PropTypes.bool,
}),
shouldBackupState: PropTypes.bool,
showResetPasswordSuccessBanner: PropTypes.bool,
submitState: PropTypes.string,
thirdPartyAuthApiStatus: PropTypes.string,
institutionLogin: PropTypes.bool.isRequired,
thirdPartyAuthContext: PropTypes.shape({
currentProvider: PropTypes.string,
errorMessage: PropTypes.string,
platformName: PropTypes.string,
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
finishAuthUrl: PropTypes.string,
}),
// Actions
backupFormState: PropTypes.func.isRequired,
dismissPasswordResetBanner: PropTypes.func.isRequired,
loginRequest: PropTypes.func.isRequired,
getTPADataFromBackend: PropTypes.func.isRequired,
handleInstitutionLogin: PropTypes.func.isRequired,
};
export default LoginPage;
LoginPage.defaultProps = {
backedUpFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
loginErrorCode: null,
loginErrorContext: {},
loginResult: {},
shouldBackupState: false,
showResetPasswordSuccessBanner: false,
submitState: DEFAULT_STATE,
thirdPartyAuthApiStatus: PENDING_STATE,
thirdPartyAuthContext: {
currentProvider: null,
errorMessage: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
};
export default connect(
mapStateToProps,
{
backupFormState: backupLoginFormBegin,
dismissPasswordResetBanner,
loginRequest,
getTPADataFromBackend: getThirdPartyAuthContext,
},
)(LoginPage);

View File

@@ -1,63 +0,0 @@
import { render, screen } from '@testing-library/react';
import '@testing-library/jest-dom';
import { LoginProvider, useLoginContext } from './LoginContext';
const TestComponent = () => {
const {
formFields,
errors,
} = useLoginContext();
return (
<div>
<div>{formFields ? 'FormFields Available' : 'FormFields Not Available'}</div>
<div>{formFields.emailOrUsername !== undefined ? 'EmailOrUsername Field Available' : 'EmailOrUsername Field Not Available'}</div>
<div>{formFields.password !== undefined ? 'Password Field Available' : 'Password Field Not Available'}</div>
<div>{errors ? 'Errors Available' : 'Errors Not Available'}</div>
<div>{errors.emailOrUsername !== undefined ? 'EmailOrUsername Error Available' : 'EmailOrUsername Error Not Available'}</div>
<div>{errors.password !== undefined ? 'Password Error Available' : 'Password Error Not Available'}</div>
</div>
);
};
describe('LoginContext', () => {
it('should render children', () => {
render(
<LoginProvider>
<div>Test Child</div>
</LoginProvider>,
);
expect(screen.getByText('Test Child')).toBeInTheDocument();
});
it('should provide all context values to children', () => {
render(
<LoginProvider>
<TestComponent />
</LoginProvider>,
);
expect(screen.getByText('FormFields Available')).toBeInTheDocument();
expect(screen.getByText('EmailOrUsername Field Available')).toBeInTheDocument();
expect(screen.getByText('Password Field Available')).toBeInTheDocument();
expect(screen.getByText('Errors Available')).toBeInTheDocument();
expect(screen.getByText('EmailOrUsername Error Available')).toBeInTheDocument();
expect(screen.getByText('Password Error Available')).toBeInTheDocument();
});
it('should render multiple children', () => {
render(
<LoginProvider>
<div>First Child</div>
<div>Second Child</div>
<div>Third Child</div>
</LoginProvider>,
);
expect(screen.getByText('First Child')).toBeInTheDocument();
expect(screen.getByText('Second Child')).toBeInTheDocument();
expect(screen.getByText('Third Child')).toBeInTheDocument();
});
});

View File

@@ -1,58 +0,0 @@
import {
createContext, FC, ReactNode, useContext, useMemo, useState,
} from 'react';
export interface FormFields {
emailOrUsername: string;
password: string;
}
export interface FormErrors {
emailOrUsername: string;
password: string;
}
interface LoginContextType {
formFields: FormFields;
setFormFields: (fields: FormFields) => void;
errors: FormErrors;
setErrors: (errors: FormErrors) => void;
}
const LoginContext = createContext<LoginContextType | undefined>(undefined);
interface LoginProviderProps {
children: ReactNode;
}
export const LoginProvider: FC<LoginProviderProps> = ({ children }) => {
const [formFields, setFormFields] = useState({
emailOrUsername: '',
password: '',
});
const [errors, setErrors] = useState({
emailOrUsername: '',
password: '',
});
const contextValue = useMemo(() => ({
formFields,
setFormFields,
errors,
setErrors,
}), [formFields, errors]);
return (
<LoginContext.Provider value={contextValue}>
{children}
</LoginContext.Provider>
);
};
export const useLoginContext = () => {
const context = useContext(LoginContext);
if (context === undefined) {
throw new Error('useLoginContext must be used within a LoginProvider');
}
return context;
};

39
src/login/data/actions.js Normal file
View File

@@ -0,0 +1,39 @@
import { AsyncActionType } from '../../data/utils';
export const BACKUP_LOGIN_DATA = new AsyncActionType('LOGIN', 'BACKUP_LOGIN_DATA');
export const LOGIN_REQUEST = new AsyncActionType('LOGIN', 'REQUEST');
export const DISMISS_PASSWORD_RESET_BANNER = 'DISMISS_PASSWORD_RESET_BANNER';
// Backup login form data
export const backupLoginForm = () => ({
type: BACKUP_LOGIN_DATA.BASE,
});
export const backupLoginFormBegin = (data) => ({
type: BACKUP_LOGIN_DATA.BEGIN,
payload: { ...data },
});
// Login
export const loginRequest = creds => ({
type: LOGIN_REQUEST.BASE,
payload: { creds },
});
export const loginRequestBegin = () => ({
type: LOGIN_REQUEST.BEGIN,
});
export const loginRequestSuccess = (redirectUrl, success) => ({
type: LOGIN_REQUEST.SUCCESS,
payload: { redirectUrl, success },
});
export const loginRequestFailure = (loginError) => ({
type: LOGIN_REQUEST.FAILURE,
payload: { loginError },
});
export const dismissPasswordResetBanner = () => ({
type: DISMISS_PASSWORD_RESET_BANNER,
});

View File

@@ -1,208 +0,0 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as QueryString from 'query-string';
import { login } from './api';
// Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
camelCaseObject: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
jest.mock('query-string', () => ({
stringify: jest.fn(),
}));
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
const mockCamelCaseObject = camelCaseObject as jest.MockedFunction<typeof camelCaseObject>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as
jest.MockedFunction<typeof getAuthenticatedHttpClient>;
const mockQueryStringify = QueryString.stringify as jest.MockedFunction<typeof QueryString.stringify>;
describe('login api', () => {
const mockHttpClient = {
post: jest.fn(),
};
const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000',
};
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
mockCamelCaseObject.mockImplementation((obj) => obj);
mockQueryStringify.mockImplementation((obj) => `stringified=${JSON.stringify(obj)}`);
});
describe('login', () => {
const mockCredentials = {
email_or_username: 'testuser@example.com',
password: 'password123',
};
const expectedUrl = `${mockConfig.LMS_BASE_URL}/api/user/v2/account/login_session/`;
const expectedConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
it('should login successfully with redirect URL', async () => {
const mockResponseData = {
redirect_url: 'http://localhost:18000/courses',
success: true,
};
const mockResponse = { data: mockResponseData };
const expectedResult = {
redirectUrl: 'http://localhost:18000/courses',
success: true,
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
mockCamelCaseObject.mockReturnValueOnce(expectedResult);
const result = await login(mockCredentials);
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
expect(mockQueryStringify).toHaveBeenCalledWith(mockCredentials);
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`stringified=${JSON.stringify(mockCredentials)}`,
expectedConfig,
);
expect(mockCamelCaseObject).toHaveBeenCalledWith({
redirectUrl: 'http://localhost:18000/courses',
success: true,
});
expect(result).toEqual(expectedResult);
});
it('should handle login failure with success false', async () => {
const mockResponseData = {
redirect_url: 'http://localhost:18000/login',
success: false,
};
const mockResponse = { data: mockResponseData };
const expectedResult = {
redirectUrl: 'http://localhost:18000/login',
success: false,
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
mockCamelCaseObject.mockReturnValueOnce(expectedResult);
const result = await login(mockCredentials);
expect(mockCamelCaseObject).toHaveBeenCalledWith({
redirectUrl: 'http://localhost:18000/login',
success: false,
});
expect(result).toEqual(expectedResult);
});
it('should properly stringify credentials using QueryString', async () => {
const complexCredentials = {
email_or_username: 'user@example.com',
password: 'pass word!@#$',
remember_me: true,
next: '/courses/course-v1:edX+DemoX+Demo_Course/courseware',
};
const mockResponse = { data: { success: true } };
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
await login(complexCredentials);
expect(mockQueryStringify).toHaveBeenCalledWith(complexCredentials);
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`stringified=${JSON.stringify(complexCredentials)}`,
expectedConfig,
);
});
it('should use correct request configuration', async () => {
const mockResponse = { data: { success: true } };
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
await login(mockCredentials);
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
expect.any(String),
{
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
},
);
});
it('should handle API error during login', async () => {
const mockError = new Error('Login API error');
mockHttpClient.post.mockRejectedValueOnce(mockError);
await expect(login(mockCredentials)).rejects.toThrow('Login API error');
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`stringified=${JSON.stringify(mockCredentials)}`,
expectedConfig
);
});
it('should handle network errors', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockHttpClient.post.mockRejectedValueOnce(networkError);
await expect(login(mockCredentials)).rejects.toThrow('Network Error');
});
it('should properly transform camelCase response', async () => {
const mockResponseData = {
redirect_url: 'http://localhost:18000/dashboard',
success: true,
user_id: 12345,
extra_data: { some: 'value' },
};
const mockResponse = { data: mockResponseData };
const expectedCamelCaseInput = {
redirectUrl: 'http://localhost:18000/dashboard',
success: true,
};
const expectedResult = {
redirectUrl: 'http://localhost:18000/dashboard',
success: true,
};
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
mockCamelCaseObject.mockReturnValueOnce(expectedResult);
const result = await login(mockCredentials);
expect(mockCamelCaseObject).toHaveBeenCalledWith(expectedCamelCaseInput);
expect(result).toEqual(expectedResult);
});
it('should handle empty credentials object', async () => {
const emptyCredentials = {};
const mockResponse = { data: { success: false } };
mockHttpClient.post.mockResolvedValueOnce(mockResponse);
await login(emptyCredentials);
expect(mockQueryStringify).toHaveBeenCalledWith(emptyCredentials);
expect(mockHttpClient.post).toHaveBeenCalledWith(
expectedUrl,
`stringified=${JSON.stringify(emptyCredentials)}`,
expectedConfig,
);
});
});
});

View File

@@ -1,236 +0,0 @@
import React from 'react';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import * as api from './api';
import {
useLogin,
} from './apiHook';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
// Mock the dependencies
jest.mock('@edx/frontend-platform/logging', () => ({
logError: jest.fn(),
logInfo: jest.fn(),
}));
jest.mock('@edx/frontend-platform/utils', () => ({
camelCaseObject: jest.fn(),
}));
jest.mock('./api', () => ({
login: jest.fn(),
}));
const mockLogin = api.login as jest.MockedFunction<typeof api.login>;
const mockLogError = logError as jest.MockedFunction<typeof logError>;
const mockLogInfo = logInfo as jest.MockedFunction<typeof logInfo>;
const mockCamelCaseObject = camelCaseObject as jest.MockedFunction<typeof camelCaseObject>;
// Test wrapper component
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function TestWrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useLogin', () => {
beforeEach(() => {
jest.clearAllMocks();
mockCamelCaseObject.mockImplementation((obj) => obj);
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useLogin(), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isSuccess).toBe(false);
expect(result.current.error).toBe(null);
});
it('should login successfully and log success', async () => {
const mockLoginData = {
email_or_username: 'testuser@example.com',
password: 'password123',
};
const mockResponse = {
redirectUrl: 'http://localhost:18000/dashboard',
success: true,
};
mockLogin.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useLogin(), {
wrapper: createWrapper(),
});
result.current.mutate(mockLoginData);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockLogin).toHaveBeenCalledWith(mockLoginData);
expect(mockLogInfo).toHaveBeenCalledWith('Login successful', mockResponse);
expect(result.current.data).toEqual(mockResponse);
});
it('should handle 400 validation error and transform to FORBIDDEN_REQUEST', async () => {
const mockLoginData = {
email_or_username: '',
password: 'password123',
};
const mockErrorResponse = {
errorCode: FORBIDDEN_REQUEST,
context: {
email_or_username: ['This field is required'],
password: ['Password is too weak'],
},
};
const mockCamelCasedResponse = {
errorCode: FORBIDDEN_REQUEST,
context: {
emailOrUsername: ['This field is required'],
password: ['Password is too weak'],
},
};
const mockError = {
response: {
status: 400,
data: mockErrorResponse,
},
};
// Mock onError callback to test formatted error
const mockOnError = jest.fn();
mockLogin.mockRejectedValueOnce(mockError);
mockCamelCaseObject.mockReturnValueOnce({
status: 400,
data: mockCamelCasedResponse,
});
const { result } = renderHook(() => useLogin({ onError: mockOnError }), {
wrapper: createWrapper(),
});
result.current.mutate(mockLoginData);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockLogin).toHaveBeenCalledWith(mockLoginData);
expect(mockCamelCaseObject).toHaveBeenCalledWith({
status: 400,
data: mockErrorResponse,
});
expect(mockLogInfo).toHaveBeenCalledWith('Login failed with validation error', mockError);
expect(mockOnError).toHaveBeenCalledWith({
type: FORBIDDEN_REQUEST,
context: {
emailOrUsername: ['This field is required'],
password: ['Password is too weak'],
},
count: 0,
});
});
it('should handle timeout errors', async () => {
const mockLoginData = {
email_or_username: 'testuser@example.com',
password: 'password123',
};
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
// Mock onError callback to test formatted error
const mockOnError = jest.fn();
mockLogin.mockRejectedValueOnce(timeoutError);
const { result } = renderHook(() => useLogin({ onError: mockOnError }), {
wrapper: createWrapper(),
});
result.current.mutate(mockLoginData);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockLogError).toHaveBeenCalledWith('Login failed', timeoutError);
expect(mockOnError).toHaveBeenCalledWith({
type: INTERNAL_SERVER_ERROR,
context: {},
count: 0,
});
});
it('should handle successful login with custom redirect URL', async () => {
const mockLoginData = {
email_or_username: 'testuser@example.com',
password: 'password123',
};
const mockResponse = {
redirectUrl: 'http://localhost:18000/courses',
success: true,
};
mockLogin.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useLogin(), {
wrapper: createWrapper(),
});
result.current.mutate(mockLoginData);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockLogInfo).toHaveBeenCalledWith('Login successful', mockResponse);
expect(result.current.data).toEqual(mockResponse);
});
it('should handle login with empty credentials', async () => {
const mockLoginData = {
email_or_username: '',
password: '',
};
const mockResponse = {
redirectUrl: 'http://localhost:18000/dashboard',
success: false,
};
mockLogin.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useLogin(), {
wrapper: createWrapper(),
});
result.current.mutate(mockLoginData);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(result.current.data).toEqual(mockResponse);
expect(mockLogInfo).toHaveBeenCalledWith('Login successful', mockResponse);
});
});

View File

@@ -1,64 +0,0 @@
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { camelCaseObject } from '@edx/frontend-platform/utils';
import { useMutation } from '@tanstack/react-query';
import { login } from './api';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
// Type definitions
interface LoginData {
email_or_username: string;
password: string;
}
interface LoginResponse {
redirectUrl?: string;
}
interface UseLoginOptions {
onSuccess?: (data: LoginResponse) => void;
onError?: (error: unknown) => void;
}
const useLogin = (options: UseLoginOptions = {}) => useMutation<LoginResponse, unknown, LoginData>({
mutationFn: async (loginData: LoginData) => login(loginData) as Promise<LoginResponse>,
onSuccess: (data: LoginResponse) => {
logInfo('Login successful', data);
if (options.onSuccess) {
options.onSuccess(data);
}
},
onError: (error: unknown) => {
logError('Login failed', error);
let formattedError = {
type: INTERNAL_SERVER_ERROR,
context: {},
count: 0,
};
if (error && typeof error === 'object' && 'response' in error && error.response) {
const response = error.response as { status?: number; data?: unknown };
const { status, data } = camelCaseObject(response);
if (data && typeof data === 'object') {
const errorData = data as { errorCode?: string; context?: { failureCount?: number } };
formattedError = {
type: errorData.errorCode || FORBIDDEN_REQUEST,
context: errorData.context || {},
count: errorData.context?.failureCount || 0,
};
if (status === 400) {
logInfo('Login failed with validation error', error);
} else if (status === 403) {
logInfo('Login failed with forbidden error', error);
} else {
logError('Login failed with server error', error);
}
}
}
if (options.onError) {
options.onError(formattedError);
}
},
});
export {
useLogin,
};

View File

@@ -0,0 +1,76 @@
import {
BACKUP_LOGIN_DATA,
DISMISS_PASSWORD_RESET_BANNER,
LOGIN_REQUEST,
} from './actions';
import { DEFAULT_STATE, PENDING_STATE } from '../../data/constants';
import { RESET_PASSWORD } from '../../reset-password';
export const defaultState = {
loginErrorCode: '',
loginErrorContext: {},
loginResult: {},
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
shouldBackupState: false,
showResetPasswordSuccessBanner: false,
submitState: DEFAULT_STATE,
};
const reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case BACKUP_LOGIN_DATA.BASE:
return {
...state,
shouldBackupState: true,
};
case BACKUP_LOGIN_DATA.BEGIN:
return {
...defaultState,
loginFormData: { ...action.payload },
};
case LOGIN_REQUEST.BEGIN:
return {
...state,
showResetPasswordSuccessBanner: false,
submitState: PENDING_STATE,
};
case LOGIN_REQUEST.SUCCESS:
return {
...state,
loginResult: action.payload,
};
case LOGIN_REQUEST.FAILURE: {
const { email, loginError, redirectUrl } = action.payload;
return {
...state,
loginErrorCode: loginError.errorCode,
loginErrorContext: { ...loginError.context, email, redirectUrl },
submitState: DEFAULT_STATE,
};
}
case RESET_PASSWORD.SUCCESS:
return {
...state,
showResetPasswordSuccessBanner: true,
};
case DISMISS_PASSWORD_RESET_BANNER: {
return {
...state,
showResetPasswordSuccessBanner: false,
};
}
default:
return {
...state,
};
}
};
export default reducer;

46
src/login/data/sagas.js Normal file
View File

@@ -0,0 +1,46 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { logError, logInfo } from '@edx/frontend-platform/logging';
import { call, put, takeEvery } from 'redux-saga/effects';
import {
LOGIN_REQUEST,
loginRequestBegin,
loginRequestFailure,
loginRequestSuccess,
} from './actions';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from './constants';
import {
loginRequest,
} from './service';
export function* handleLoginRequest(action) {
try {
yield put(loginRequestBegin());
const { redirectUrl, success } = yield call(loginRequest, action.payload.creds);
yield put(loginRequestSuccess(
redirectUrl,
success,
));
} catch (e) {
const statusCodes = [400];
if (e.response) {
const { status } = e.response;
if (statusCodes.includes(status)) {
yield put(loginRequestFailure(camelCaseObject(e.response.data)));
logInfo(e);
} else if (status === 403) {
yield put(loginRequestFailure({ errorCode: FORBIDDEN_REQUEST }));
logInfo(e);
} else {
yield put(loginRequestFailure({ errorCode: INTERNAL_SERVER_ERROR }));
logError(e);
}
}
}
}
export default function* saga() {
yield takeEvery(LOGIN_REQUEST.BASE, handleLoginRequest);
}

View File

@@ -1,21 +1,26 @@
import { camelCaseObject, getConfig } from '@edx/frontend-platform';
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import * as QueryString from 'query-string';
const login = async (creds) => {
// eslint-disable-next-line import/prefer-default-export
export async function loginRequest(creds) {
const requestConfig = {
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
isPublic: true,
};
const url = `${getConfig().LMS_BASE_URL}/api/user/v2/account/login_session/`;
const { data } = await getAuthenticatedHttpClient()
.post(url, QueryString.stringify(creds), requestConfig);
return camelCaseObject({
.post(
`${getConfig().LMS_BASE_URL}/api/user/v2/account/login_session/`,
QueryString.stringify(creds),
requestConfig,
)
.catch((e) => {
throw (e);
});
return {
redirectUrl: data.redirect_url || `${getConfig().LMS_BASE_URL}/dashboard`,
success: data.success || false,
});
};
export {
login,
};
};
}

View File

@@ -0,0 +1,155 @@
import { getConfig } from '@edx/frontend-platform';
import { DEFAULT_REDIRECT_URL, DEFAULT_STATE, PENDING_STATE } from '../../../data/constants';
import { RESET_PASSWORD } from '../../../reset-password';
import { BACKUP_LOGIN_DATA, DISMISS_PASSWORD_RESET_BANNER, LOGIN_REQUEST } from '../actions';
import reducer from '../reducers';
describe('login reducer', () => {
const defaultState = {
loginErrorCode: '',
loginErrorContext: {},
loginResult: {},
loginFormData: {
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
shouldBackupState: false,
showResetPasswordSuccessBanner: false,
submitState: DEFAULT_STATE,
};
it('should update state to show reset password success banner', () => {
const action = {
type: RESET_PASSWORD.SUCCESS,
};
expect(
reducer(defaultState, action),
).toEqual(
{
...defaultState,
showResetPasswordSuccessBanner: true,
},
);
});
it('should set the flag which keeps the login form data in redux state', () => {
const action = {
type: BACKUP_LOGIN_DATA.BASE,
};
expect(
reducer(defaultState, action),
).toEqual(
{
...defaultState,
shouldBackupState: true,
},
);
});
it('should backup the login form data', () => {
const payload = {
formFields: {
emailOrUsername: 'test@exmaple.com',
password: 'test1',
},
errors: {
emailOrUsername: '', password: '',
},
};
const action = {
type: BACKUP_LOGIN_DATA.BEGIN,
payload,
};
expect(
reducer(defaultState, action),
).toEqual(
{
...defaultState,
loginFormData: payload,
},
);
});
it('should update state to dismiss reset password banner', () => {
const action = {
type: DISMISS_PASSWORD_RESET_BANNER,
};
expect(
reducer(defaultState, action),
).toEqual(
{
...defaultState,
showResetPasswordSuccessBanner: false,
},
);
});
it('should start the login request', () => {
const action = {
type: LOGIN_REQUEST.BEGIN,
};
expect(reducer(defaultState, action)).toEqual(
{
...defaultState,
showResetPasswordSuccessBanner: false,
submitState: PENDING_STATE,
},
);
});
it('should set redirect url on login success action', () => {
const payload = {
redirectUrl: `${getConfig().BASE_URL}${DEFAULT_REDIRECT_URL}`,
success: true,
};
const action = {
type: LOGIN_REQUEST.SUCCESS,
payload,
};
expect(reducer(defaultState, action)).toEqual(
{
...defaultState,
loginResult: payload,
},
);
});
it('should set the error data on login request failure', () => {
const payload = {
loginError: {
success: false,
value: 'Email or password is incorrect.',
errorCode: 'incorrect-email-or-password',
context: {
failureCount: 0,
},
},
email: 'test@example.com',
redirectUrl: '',
};
const action = {
type: LOGIN_REQUEST.FAILURE,
payload,
};
expect(reducer(defaultState, action)).toEqual(
{
...defaultState,
loginErrorCode: payload.loginError.errorCode,
loginErrorContext: { ...payload.loginError.context, email: payload.email, redirectUrl: payload.redirectUrl },
submitState: DEFAULT_STATE,
},
);
});
});

View File

@@ -0,0 +1,110 @@
import { camelCaseObject } from '@edx/frontend-platform';
import { runSaga } from 'redux-saga';
import initializeMockLogging from '../../../setupTest';
import * as actions from '../actions';
import { FORBIDDEN_REQUEST, INTERNAL_SERVER_ERROR } from '../constants';
import { handleLoginRequest } from '../sagas';
import * as api from '../service';
const { loggingService } = initializeMockLogging();
describe('handleLoginRequest', () => {
const params = {
payload: {
loginFormData: {
email: 'test@test.com',
password: 'test-password',
},
},
};
const testErrorResponse = async (loginErrorResponse, expectedLogFunc, expectedDispatchers) => {
const loginRequest = jest.spyOn(api, 'loginRequest').mockImplementation(() => Promise.reject(loginErrorResponse));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleLoginRequest,
params,
);
expect(loginRequest).toHaveBeenCalledTimes(1);
expect(expectedLogFunc).toHaveBeenCalled();
expect(dispatched).toEqual(expectedDispatchers);
loginRequest.mockClear();
};
beforeEach(() => {
loggingService.logError.mockReset();
loggingService.logInfo.mockReset();
});
it('should call service and dispatch success action', async () => {
const data = { redirectUrl: '/dashboard', success: true };
const loginRequest = jest.spyOn(api, 'loginRequest')
.mockImplementation(() => Promise.resolve(data));
const dispatched = [];
await runSaga(
{ dispatch: (action) => dispatched.push(action) },
handleLoginRequest,
params,
);
expect(loginRequest).toHaveBeenCalledTimes(1);
expect(dispatched).toEqual([
actions.loginRequestBegin(),
actions.loginRequestSuccess(data.redirectUrl, data.success),
]);
loginRequest.mockClear();
});
it('should call service and dispatch error action', async () => {
const loginErrorResponse = {
response: {
status: 400,
data: {
login_error: 'something went wrong',
},
},
};
await testErrorResponse(loginErrorResponse, loggingService.logInfo, [
actions.loginRequestBegin(),
actions.loginRequestFailure(camelCaseObject(loginErrorResponse.response.data)),
]);
});
it('should handle rate limit error code', async () => {
const loginErrorResponse = {
response: {
status: 403,
data: {
errorCode: FORBIDDEN_REQUEST,
},
},
};
await testErrorResponse(loginErrorResponse, loggingService.logInfo, [
actions.loginRequestBegin(),
actions.loginRequestFailure(loginErrorResponse.response.data),
]);
});
it('should handle 500 error code', async () => {
const loginErrorResponse = {
response: {
status: 500,
data: {
errorCode: INTERNAL_SERVER_ERROR,
},
},
};
await testErrorResponse(loginErrorResponse, loggingService.logError, [
actions.loginRequestBegin(),
actions.loginRequestFailure(loginErrorResponse.response.data),
]);
});
});

View File

@@ -1,3 +1,5 @@
export const storeName = 'login';
export { default as LoginPage } from './LoginPage';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

View File

@@ -1,27 +1,20 @@
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import {
fireEvent, render, screen, waitFor,
} from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import { useThirdPartyAuthContext } from '../../common-components/components/ThirdPartyAuthContext';
import { useThirdPartyAuthHook } from '../../common-components/data/apiHook';
import { COMPLETE_STATE, LOGIN_PAGE, PENDING_STATE } from '../../data/constants';
import { RegisterProvider } from '../../register/components/RegisterContext';
import { LoginProvider } from '../components/LoginContext';
import { useLogin } from '../data/apiHook';
import { backupLoginFormBegin, dismissPasswordResetBanner, loginRequest } from '../data/actions';
import { INTERNAL_SERVER_ERROR } from '../data/constants';
import LoginPage from '../LoginPage';
// Mock React Query hooks
jest.mock('../data/apiHook');
jest.mock('../../common-components/data/apiHook');
jest.mock('../../common-components/components/ThirdPartyAuthContext');
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
@@ -30,28 +23,39 @@ jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: jest.fn(),
}));
const mockStore = configureStore();
describe('LoginPage', () => {
let props = {};
let mockLoginMutate;
let mockThirdPartyAuthContext;
let queryClient;
let store = {};
const emptyFieldValidation = { emailOrUsername: 'Enter your username or email', password: 'Enter your password' };
const queryWrapper = children => (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
<RegisterProvider>
<LoginProvider>
{children}
</LoginProvider>
</RegisterProvider>
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
login: {
loginResult: { success: false, redirectUrl: '' },
},
commonComponents: {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
},
},
register: {
validationApiRateLimited: false,
},
};
const secondaryProviders = {
id: 'saml-test',
name: 'Test University',
@@ -69,121 +73,98 @@ describe('LoginPage', () => {
};
beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
mockLoginMutate = jest.fn();
mockLoginMutate.mockRejected = false; // Reset flag
const loginMutation = {
mutate: mockLoginMutate,
isPending: false,
};
useLogin.mockImplementation((options) => ({
...loginMutation,
mutate: jest.fn().mockImplementation((data) => {
// Call the mocked function for testing assertions
mockLoginMutate(data);
// Simulate can call success or error based on test needs
if (options?.onSuccess && !mockLoginMutate.mockRejected) {
options.onSuccess({ redirectUrl: 'https://test.com/dashboard' });
}
}),
}));
useThirdPartyAuthHook.mockReturnValue({
data: {
fieldDescriptions: {},
optionalFields: { fields: {}, extended_profile: [] },
thirdPartyAuthContext: {},
},
isSuccess: true,
error: null,
isLoading: false,
});
mockThirdPartyAuthContext = {
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [],
platformName: '',
errorMessage: '',
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore(initialState);
props = {
institutionLogin: false,
loginRequest: jest.fn(),
handleInstitutionLogin: jest.fn(),
institutionLogin: false,
};
});
// ******** test login form submission ********
it('should submit form for valid input', () => {
render(queryWrapper(<LoginPage {...props} />));
store.dispatch = jest.fn(store.dispatch);
fireEvent.change(screen.getByLabelText(/username or email/i), {
target: { value: 'test', name: 'emailOrUsername' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'test-password', name: 'password' },
});
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
fireEvent.change(screen.getByText(
'',
{ selector: '#emailOrUsername' },
), { target: { value: 'test', name: 'emailOrUsername' } });
fireEvent.change(screen.getByText(
'',
{ selector: '#password' },
), { target: { value: 'test-password', name: 'password' } });
expect(mockLoginMutate).toHaveBeenCalledWith({ email_or_username: 'test', password: 'test-password' });
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(store.dispatch).toHaveBeenCalledWith(loginRequest({ email_or_username: 'test', password: 'test-password' }));
});
it('should not call login mutation on empty form submission', () => {
render(queryWrapper(<LoginPage {...props} />));
it('should not dispatch loginRequest on empty form submission', () => {
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(mockLoginMutate).not.toHaveBeenCalled();
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(store.dispatch).not.toHaveBeenCalledWith(loginRequest({}));
});
it('should dismiss reset password banner on form submission', () => {
delete window.location;
window.location = {
href: getConfig().BASE_URL.concat(LOGIN_PAGE),
search: '?reset=success',
pathname: '/login',
};
store = mockStore({
...initialState,
login: {
...initialState.login,
showResetPasswordSuccessBanner: true,
},
});
const { container } = render(queryWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(container.querySelector('.alert-success, [role="alert"].alert-success')).toBeFalsy();
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(store.dispatch).toHaveBeenCalledWith(dismissPasswordResetBanner());
});
// ******** test login form validations ********
it('should match state for invalid email (less than 2 characters), on form submission', () => {
render(queryWrapper(<LoginPage {...props} />));
store.dispatch = jest.fn(store.dispatch);
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'test', name: 'password' },
});
fireEvent.change(screen.getByLabelText(/username or email/i), {
target: { value: 't', name: 'emailOrUsername' },
});
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
fireEvent.change(screen.getByText(
'',
{ selector: '#password' },
), { target: { value: 'test' } });
fireEvent.change(screen.getByText(
'',
{ selector: '#emailOrUsername' },
), { target: { value: 't' } });
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(screen.getByText('Username or email must have at least 2 characters.')).toBeDefined();
});
it('should show error messages for required fields on empty form submission', () => {
const { container } = render(queryWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
const { container } = render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual(emptyFieldValidation.emailOrUsername);
expect(container.querySelector('div[feedback-for="password"]').textContent).toEqual(emptyFieldValidation.password);
@@ -193,28 +174,43 @@ describe('LoginPage', () => {
});
it('should run frontend validations for emailOrUsername field on form submission', () => {
const { container } = render(queryWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByLabelText(/username or email/i), {
target: { value: 't', name: 'emailOrUsername' },
});
fireEvent.change(screen.getByText(
'',
{ selector: '#emailOrUsername' },
), { target: { value: 't', name: 'emailOrUsername' } });
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
expect(container.querySelector('div[feedback-for="emailOrUsername"]').textContent).toEqual('Username or email must have at least 2 characters.');
});
// ******** test field focus in functionality ********
it('should reset field related error messages on onFocus event', async () => {
render(queryWrapper(<LoginPage {...props} />));
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
await act(async () => {
// clicking submit button with empty fields to make the errors appear
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
fireEvent.click(screen.getByText(
'',
{ selector: '.btn-brand' },
));
// focusing the fields to verify that the errors are cleared
fireEvent.focus(screen.getByLabelText('Password'));
fireEvent.focus(screen.getByLabelText(/username or email/i));
fireEvent.focus(screen.getByText(
'',
{ selector: '#password' },
));
fireEvent.focus(screen.getByText(
'',
{ selector: '#emailOrUsername' },
));
});
// verifying that the errors are cleared
@@ -226,17 +222,20 @@ describe('LoginPage', () => {
// ******** test form buttons and links ********
it('should match default button state', () => {
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText('Sign in')).toBeDefined();
});
it('should match pending button state', () => {
useLogin.mockReturnValue({
mutate: mockLoginMutate,
isPending: true,
store = mockStore({
...initialState,
login: {
...initialState.login,
submitState: PENDING_STATE,
},
});
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'pending',
@@ -244,7 +243,7 @@ describe('LoginPage', () => {
});
it('should show forgot password link', () => {
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'Forgot password',
@@ -253,10 +252,18 @@ describe('LoginPage', () => {
});
it('should show single sign on provider button', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext.providers = [ssoProvider];
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
},
});
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: `#${ssoProvider.id}` },
@@ -268,27 +275,37 @@ describe('LoginPage', () => {
});
it('should display sign-in header only when primary or secondary providers are available.', () => {
// Reset mocks to empty providers
mockThirdPartyAuthContext.thirdPartyAuthContext.providers = [];
mockThirdPartyAuthContext.thirdPartyAuthContext.secondaryProviders = [];
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
});
it('should hide sign-in header and enterprise login upon successful SSO authentication', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
currentProvider: 'Apple',
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
currentProvider: 'Apple',
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Or sign in with:')).toBeNull();
});
@@ -296,14 +313,19 @@ describe('LoginPage', () => {
// ******** test enterprise login enabled scenarios ********
it('should show sign-in header for enterprise login', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -316,14 +338,19 @@ describe('LoginPage', () => {
DISABLE_ENTERPRISE_LOGIN: true,
});
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
secondaryProviders: [secondaryProviders],
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -338,15 +365,20 @@ describe('LoginPage', () => {
DISABLE_ENTERPRISE_LOGIN: true,
});
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
secondaryProviders: [{
...secondaryProviders,
}],
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [{
...secondaryProviders,
}],
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -356,21 +388,35 @@ describe('LoginPage', () => {
});
it('should not show sign-in header without primary or secondary providers', () => {
// Already mocked with empty providers in beforeEach
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
},
},
});
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeNull();
expect(queryByText('Company or school credentials')).toBeNull();
});
it('should show enterprise login if even if only secondary providers are available', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
},
});
const { queryByText } = render(queryWrapper(<LoginPage {...props} />));
const { queryByText } = render(reduxWrapper(<LoginPage {...props} />));
expect(queryByText('Or sign in with:')).toBeDefined();
expect(queryByText('Company or school credentials')).toBeNull();
expect(queryByText('Institution/campus credentials')).toBeDefined();
@@ -382,55 +428,42 @@ describe('LoginPage', () => {
// ******** test alert messages ********
// Login error handling is now managed by React Query hooks and context
// We'll test that error messages appear when login fails
it('should show error message when login fails', async () => {
// Mock the login hook to simulate error
mockLoginMutate.mockRejected = true;
useLogin.mockImplementation((options) => ({
mutate: jest.fn().mockImplementation((data) => {
mockLoginMutate(data);
if (options?.onError) {
options.onError({ type: INTERNAL_SERVER_ERROR, context: {}, count: 0 });
}
}),
isPending: false,
}));
useLogin.mockReturnValue({
mutate: mockLoginMutate,
isPending: false,
it('should match login internal server error message', () => {
const expectedMessage = 'We couldn\'t sign you in.'
+ 'An error has occurred. Try refreshing the page, or check your internet connection.';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginErrorCode: INTERNAL_SERVER_ERROR,
},
});
render(queryWrapper(<LoginPage {...props} />));
// Fill in valid form data
fireEvent.change(screen.getByLabelText(/username or email/i), {
target: { value: 'test@example.com', name: 'emailOrUsername' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123', name: 'password' },
});
// Submit form
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
// The error should be handled by the login hook
expect(mockLoginMutate).toHaveBeenCalled();
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
).textContent).toEqual(`${expectedMessage}`);
});
it('should match third party auth alert', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
currentProvider: 'Apple',
platformName: 'openedX',
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: 'Apple',
platformName: 'openedX',
},
},
});
const expectedMessage = `${'You have successfully signed into Apple, but your Apple account does not have a '
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${getConfig().SITE_NAME } password.`;
+ 'linked '}${ getConfig().SITE_NAME } account. To link your accounts, sign in now using your ${
getConfig().SITE_NAME } password.`;
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#tpa-alert' },
@@ -438,96 +471,105 @@ describe('LoginPage', () => {
});
it('should show third party authentication failure message', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
currentProvider: null,
errorMessage: 'An error occurred',
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
render(queryWrapper(<LoginPage {...props} />));
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
currentProvider: null,
errorMessage: 'An error occurred',
},
},
});
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
).textContent).toContain('An error occurred');
});
// Form validation errors are now handled by context
it('should show form validation error', () => {
render(queryWrapper(<LoginPage {...props} />));
it('should match invalid login form error message', () => {
const errorMessage = 'Please fill in the fields below.';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginErrorCode: 'invalid-form',
},
});
// Submit form without filling fields
fireEvent.click(screen.getByText('Sign in'));
// Should show validation errors
expect(screen.getByText('Please fill in the fields below.')).toBeDefined();
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: '#login-failure-alert' },
).textContent).toContain(errorMessage);
});
// ******** test redirection ********
// Login success and redirection is now handled by React Query hooks
it('should handle successful login', () => {
// Mock successful login
useLogin.mockImplementation((options) => ({
mutate: jest.fn().mockImplementation((data) => {
mockLoginMutate(data);
if (options?.onSuccess) {
options.onSuccess({ success: true, redirectUrl: 'https://test.com/testing-dashboard/' });
}
}),
isPending: false,
}));
useLogin.mockReturnValue({
mutate: mockLoginMutate,
isPending: false,
it('should redirect to url returned by login endpoint after successful authentication', () => {
const dashboardURL = 'https://test.com/testing-dashboard/';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: dashboardURL,
},
},
});
render(queryWrapper(<LoginPage {...props} />));
// Fill in valid form data
fireEvent.change(screen.getByLabelText('Username or email'), {
target: { value: 'test@example.com', name: 'emailOrUsername' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'password123', name: 'password' },
});
// Submit form
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
expect(mockLoginMutate).toHaveBeenCalled();
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(dashboardURL);
});
it('should handle SSO login success', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
finishAuthUrl: '/auth/complete/google-oauth2/',
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
// Mock successful login with no redirect URL (SSO case)
mockLoginMutate.mockImplementation((payload, { onSuccess }) => {
onSuccess({ success: true, redirectUrl: '' });
it('should redirect to finishAuthUrl upon successful login via SSO', () => {
const authCompleteUrl = '/auth/complete/google-oauth2/';
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: {
success: true,
redirectUrl: '',
},
},
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
finishAuthUrl: authCompleteUrl,
},
},
});
render(queryWrapper(<LoginPage {...props} />));
// The component should handle SSO success
expect(mockThirdPartyAuthContext.thirdPartyAuthContext.finishAuthUrl).toBe('/auth/complete/google-oauth2/');
});
it('should redirect to social auth provider url on SSO button click', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + authCompleteUrl);
});
it('should redirect to social auth provider url on SSO button click', () => {
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL };
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'',
@@ -536,34 +578,49 @@ describe('LoginPage', () => {
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + ssoProvider.loginUrl);
});
it('should handle successful authentication via SSO', () => {
it('should redirect to finishAuthUrl upon successful authentication via SSO', () => {
const finishAuthUrl = '/auth/complete/google-oauth2/';
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
finishAuthUrl,
};
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
login: {
...initialState.login,
loginResult: { success: true, redirectUrl: '' },
},
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
finishAuthUrl,
},
},
});
render(queryWrapper(<LoginPage {...props} />));
delete window.location;
window.location = { href: getConfig().BASE_URL };
// Verify the finish auth URL is available
expect(mockThirdPartyAuthContext.thirdPartyAuthContext.finishAuthUrl).toBe(finishAuthUrl);
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toBe(getConfig().LMS_BASE_URL + finishAuthUrl);
});
// ******** test hinted third party auth ********
it('should render tpa button for tpa_hint id matching one of the primary providers', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = COMPLETE_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'',
{ selector: `#${ssoProvider.id}` },
@@ -575,49 +632,64 @@ describe('LoginPage', () => {
});
it('should render the skeleton when third party status is pending', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = PENDING_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
thirdPartyAuthApiStatus: PENDING_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${ssoProvider.id}` };
const { container } = render(queryWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector('.react-loading-skeleton')).toBeTruthy();
});
it('should render tpa button for tpa_hint id matching one of the secondary providers', () => {
secondaryProviders.skipHintedLogin = true;
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = COMPLETE_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?next=/dashboard&tpa_hint=${secondaryProviders.id}` };
secondaryProviders.iconImage = null;
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(window.location.href).toEqual(getConfig().LMS_BASE_URL + secondaryProviders.loginUrl);
});
it('should render regular tpa button for invalid tpa_hint value', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = COMPLETE_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: '?next=/dashboard&tpa_hint=invalid' };
const { container } = render(queryWrapper(<LoginPage {...props} />));
const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector(`#${ssoProvider.id}`).querySelector('#provider-name').textContent).toEqual(`${ssoProvider.name}`);
mergeConfig({
@@ -626,17 +698,22 @@ describe('LoginPage', () => {
});
it('should render "other ways to sign in" button on the tpa_hint page', () => {
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = COMPLETE_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'Show me other ways to sign in or register',
).textContent).toBeDefined();
@@ -647,17 +724,22 @@ describe('LoginPage', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: false,
});
mockThirdPartyAuthContext.thirdPartyAuthContext = {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
providers: [ssoProvider],
};
mockThirdPartyAuthContext.thirdPartyAuthApiStatus = COMPLETE_STATE;
useThirdPartyAuthContext.mockReturnValue(mockThirdPartyAuthContext);
store = mockStore({
...initialState,
commonComponents: {
...initialState.commonComponents,
thirdPartyAuthContext: {
...initialState.commonComponents.thirdPartyAuthContext,
providers: [ssoProvider],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { href: getConfig().BASE_URL.concat(LOGIN_PAGE), search: `?tpa_hint=${ssoProvider.id}` };
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(screen.getByText(
'Show me other ways to sign in',
).textContent).toBeDefined();
@@ -666,25 +748,35 @@ describe('LoginPage', () => {
// ******** miscellaneous tests ********
it('should send page event when login page is rendered', () => {
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'login');
});
it('should handle form field changes', () => {
render(queryWrapper(<LoginPage {...props} />));
it('tests that form is in invalid state when it is submitted', () => {
store = mockStore({
...initialState,
login: {
...initialState.login,
shouldBackupState: true,
},
});
const emailInput = screen.getByLabelText(/username or email/i);
const passwordInput = screen.getByLabelText('Password');
fireEvent.change(emailInput, { target: { value: 'test@example.com', name: 'emailOrUsername' } });
fireEvent.change(passwordInput, { target: { value: 'password123', name: 'password' } });
expect(emailInput.value).toBe('test@example.com');
expect(passwordInput.value).toBe('password123');
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
));
});
it('should send track event when forgot password link is clicked', () => {
render(queryWrapper(<LoginPage {...props} />));
render(reduxWrapper(<LoginPage {...props} />));
fireEvent.click(screen.getByText(
'Forgot password',
{ selector: '#forgot-password' },
@@ -693,91 +785,47 @@ describe('LoginPage', () => {
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.password-reset_form.toggled', { category: 'user-engagement' });
});
it('should persist and load form fields using sessionStorage', () => {
const { container, rerender } = render(queryWrapper(<LoginPage {...props} />));
fireEvent.change(container.querySelector('input#emailOrUsername'), {
target: { value: 'john_doe', name: 'emailOrUsername' },
});
fireEvent.change(container.querySelector('input#password'), {
target: { value: 'test-password', name: 'password' },
});
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password');
rerender(queryWrapper(<LoginPage {...props} />));
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password');
});
it('should prevent default on mouseDown event for sign-in button', () => {
const { container } = render(queryWrapper(<LoginPage {...props} />));
const signInButton = container.querySelector('#sign-in');
const preventDefaultSpy = jest.fn();
const event = new Event('mousedown', { bubbles: true });
event.preventDefault = preventDefaultSpy;
signInButton.dispatchEvent(event);
expect(preventDefaultSpy).toHaveBeenCalled();
});
it('should call setThirdPartyAuthContextSuccess on successful third party auth fetch', async () => {
render(queryWrapper(<LoginPage {...props} />));
await waitFor(() => {
expect(mockThirdPartyAuthContext.setThirdPartyAuthContextSuccess).toHaveBeenCalled();
}, { timeout: 1000 });
});
it('should call setThirdPartyAuthContextFailure on failed third party auth fetch', async () => {
useThirdPartyAuthHook.mockReturnValue({
data: null,
isSuccess: false,
error: new Error('Network error'),
isLoading: false,
});
render(queryWrapper(<LoginPage {...props} />));
await waitFor(() => {
expect(mockThirdPartyAuthContext.setThirdPartyAuthContextFailure).toHaveBeenCalled();
});
});
it('should set error code when third party error message is present', async () => {
const contextWithError = {
...mockThirdPartyAuthContext,
thirdPartyAuthContext: {
...mockThirdPartyAuthContext.thirdPartyAuthContext,
errorMessage: 'Third party authentication failed',
it('should backup the login form state when shouldBackupState is true', () => {
store = mockStore({
...initialState,
login: {
...initialState.login,
shouldBackupState: true,
},
};
useThirdPartyAuthContext.mockReturnValue(contextWithError);
const { container } = render(queryWrapper(<LoginPage {...props} />));
await waitFor(() => {
expect(container.querySelector('.alert-danger, .alert, [role="alert"]')).toBeTruthy();
});
store.dispatch = jest.fn(store.dispatch);
render(reduxWrapper(<LoginPage {...props} />));
expect(store.dispatch).toHaveBeenCalledWith(backupLoginFormBegin(
{
formFields: {
emailOrUsername: '', password: '',
},
errors: {
emailOrUsername: '', password: '',
},
},
));
});
it('should set error code on login failure', async () => {
mockLoginMutate.mockRejected = true;
useLogin.mockImplementation((options) => ({
mutate: jest.fn().mockImplementation((data) => {
mockLoginMutate(data);
if (options?.onError) {
options.onError({ type: INTERNAL_SERVER_ERROR, context: {}, count: 0 });
}
}),
isPending: false,
}));
it('should update form fields state if updated in redux store', () => {
store = mockStore({
...initialState,
login: {
...initialState.login,
loginFormData: {
formFields: {
emailOrUsername: 'john_doe', password: 'test-password',
},
errors: {
emailOrUsername: '', password: '',
},
},
},
});
const { container } = render(queryWrapper(<LoginPage {...props} />));
fireEvent.change(screen.getByLabelText(/username or email/i), {
target: { value: 'test', name: 'emailOrUsername' },
});
fireEvent.change(screen.getByLabelText('Password'), {
target: { value: 'test-password', name: 'password' },
});
fireEvent.click(screen.getByRole('button', { name: /sign in/i }));
await waitFor(() => {
expect(container.querySelector('.alert-danger, .alert, [role="alert"]')).toBeTruthy();
});
const { container } = render(reduxWrapper(<LoginPage {...props} />));
expect(container.querySelector('input#emailOrUsername').value).toEqual('john_doe');
expect(container.querySelector('input#password').value).toEqual('test-password');
});
});

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -14,31 +15,26 @@ import PropTypes from 'prop-types';
import { Navigate, useNavigate } from 'react-router-dom';
import BaseContainer from '../base-container';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from '../common-components/components/ThirdPartyAuthContext';
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
tpaProvidersSelector,
} from '../common-components/data/selectors';
import messages from '../common-components/messages';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
import {
getTpaHint, getTpaProvider, updatePathWithQueryParams,
} from '../data/utils';
import { LoginProvider } from '../login/components/LoginContext';
import LoginComponentSlot from '../plugin-slots/LoginComponentSlot';
import { LoginPage } from '../login';
import { backupLoginForm } from '../login/data/actions';
import { RegistrationPage } from '../register';
import { RegisterProvider } from '../register/components/RegisterContext';
import { backupRegistrationForm } from '../register/data/actions';
const LogistrationPageInner = ({
selectedPage,
}) => {
const Logistration = (props) => {
const { selectedPage, tpaProviders } = props;
const tpaHint = getTpaHint();
const {
thirdPartyAuthContext,
clearThirdPartyAuthErrorMessage,
} = useThirdPartyAuthContext();
const {
providers,
secondaryProviders,
} = thirdPartyAuthContext;
providers, secondaryProviders,
} = tpaProviders;
const { formatMessage } = useIntl();
const [institutionLogin, setInstitutionLogin] = useState(false);
const [key, setKey] = useState('');
@@ -49,10 +45,9 @@ const LogistrationPageInner = ({
useEffect(() => {
const authService = getAuthService();
if (authService) {
authService.getCsrfTokenService()
.getCsrfToken(getConfig().LMS_BASE_URL);
authService.getCsrfTokenService().getCsrfToken(getConfig().LMS_BASE_URL);
}
}, []);
});
useEffect(() => {
if (disablePublicAccountCreation) {
@@ -67,6 +62,7 @@ const LogistrationPageInner = ({
} else {
sendPageEvent('login_and_registration', e.target.dataset.eventName);
}
setInstitutionLogin(!institutionLogin);
};
@@ -75,7 +71,12 @@ const LogistrationPageInner = ({
return;
}
sendTrackEvent(`edx.bi.${tabKey.replace('/', '')}_form.toggled`, { category: 'user-engagement' });
clearThirdPartyAuthErrorMessage();
props.clearThirdPartyAuthContextErrorMessage();
if (tabKey === LOGIN_PAGE) {
props.backupRegistrationForm();
} else if (tabKey === REGISTER_PAGE) {
props.backupLoginForm();
}
setKey(tabKey);
};
@@ -110,10 +111,7 @@ const LogistrationPageInner = ({
{!institutionLogin && (
<h3 className="mb-4.5">{formatMessage(messages['logistration.sign.in'])}</h3>
)}
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
<LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
</div>
</>
)
@@ -126,16 +124,12 @@ const LogistrationPageInner = ({
</Tabs>
)
: (!isValidTpaHint() && !hideRegistrationLink && (
<Tabs
defaultActiveKey={selectedPage}
id="controlled-tab"
onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}
>
<Tabs defaultActiveKey={selectedPage} id="controlled-tab" onSelect={(tabKey) => handleOnSelect(tabKey, selectedPage)}>
<Tab title={formatMessage(messages['logistration.register'])} eventKey={REGISTER_PAGE} />
<Tab title={formatMessage(messages['logistration.sign.in'])} eventKey={LOGIN_PAGE} />
</Tabs>
))}
{key && (
{ key && (
<Navigate to={updatePathWithQueryParams(key)} replace />
)}
<div id="main-content" className="main-content">
@@ -145,12 +139,7 @@ const LogistrationPageInner = ({
</h3>
)}
{selectedPage === LOGIN_PAGE
? (
<LoginComponentSlot
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
)
? <LoginPage institutionLogin={institutionLogin} handleInstitutionLogin={handleInstitutionLogin} />
: (
<RegistrationPage
institutionLogin={institutionLogin}
@@ -165,21 +154,37 @@ const LogistrationPageInner = ({
);
};
LogistrationPageInner.propTypes = {
selectedPage: PropTypes.string.isRequired,
Logistration.propTypes = {
selectedPage: PropTypes.string,
backupLoginForm: PropTypes.func.isRequired,
backupRegistrationForm: PropTypes.func.isRequired,
clearThirdPartyAuthContextErrorMessage: PropTypes.func.isRequired,
tpaProviders: PropTypes.shape({
providers: PropTypes.arrayOf(PropTypes.shape({})),
secondaryProviders: PropTypes.arrayOf(PropTypes.shape({})),
}),
};
/**
* Main Logistration Page component wrapped with providers
*/
const LogistrationPage = (props) => (
<ThirdPartyAuthProvider>
<RegisterProvider>
<LoginProvider>
<LogistrationPageInner {...props} />
</LoginProvider>
</RegisterProvider>
</ThirdPartyAuthProvider>
);
Logistration.defaultProps = {
tpaProviders: {
providers: [],
secondaryProviders: [],
},
};
export default LogistrationPage;
Logistration.defaultProps = {
selectedPage: REGISTER_PAGE,
};
const mapStateToProps = state => ({
tpaProviders: tpaProvidersSelector(state),
});
export default connect(
mapStateToProps,
{
backupLoginForm,
backupRegistrationForm,
clearThirdPartyAuthContextErrorMessage,
},
)(Logistration);

View File

@@ -1,166 +1,87 @@
import { Provider } from 'react-redux';
import { getConfig, mergeConfig } from '@edx/frontend-platform';
import { sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
import { configure, IntlProvider } from '@edx/frontend-platform/i18n';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { fireEvent, render, screen } from '@testing-library/react';
import { MemoryRouter } from 'react-router-dom';
import configureStore from 'redux-mock-store';
import Logistration from './Logistration';
import { LOGIN_PAGE, REGISTER_PAGE } from '../data/constants';
// Mock the navigate function
const mockNavigate = jest.fn();
const mockGetCsrfToken = jest.fn();
jest.mock('react-router-dom', () => ({
...jest.requireActual('react-router-dom'),
useNavigate: () => mockNavigate,
Navigate: ({ to }) => {
mockNavigate(to);
return null;
},
}));
import { clearThirdPartyAuthContextErrorMessage } from '../common-components/data/actions';
import {
COMPLETE_STATE, LOGIN_PAGE, REGISTER_PAGE,
} from '../data/constants';
import { backupLoginForm } from '../login/data/actions';
import { backupRegistrationForm } from '../register/data/actions';
jest.mock('@edx/frontend-platform/analytics', () => ({
sendPageEvent: jest.fn(),
sendTrackEvent: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthService: () => ({
getCsrfTokenService: () => ({
getCsrfToken: mockGetCsrfToken,
}),
}),
}));
jest.mock('@edx/frontend-platform', () => ({
...jest.requireActual('@edx/frontend-platform'),
getConfig: jest.fn(() => ({
ALLOW_PUBLIC_ACCOUNT_CREATION: 'true',
DISABLE_ENTERPRISE_LOGIN: 'true',
SHOW_REGISTRATION_LINKS: 'true',
PROVIDERS: [],
SECONDARY_PROVIDERS: [{
id: 'saml-test_university',
name: 'Test University',
iconClass: 'fa-university',
iconImage: null,
loginUrl: '/auth/login/saml-test_university/?auth_entry=login&next=%2Fdashboard',
registerUrl: '/auth/login/saml-test_university/?auth_entry=register&next=%2Fdashboard',
}],
TPA_HINT: '',
TPA_PROVIDER_ID: '',
})),
}));
jest.mock('@edx/frontend-platform/auth');
// Mock the apiHook to prevent logging errors
jest.mock('../common-components/data/apiHook', () => ({
useLoginMutation: jest.fn(() => ({
mutate: jest.fn(),
isLoading: false,
error: null,
})),
useThirdPartyAuthMutation: jest.fn(() => ({
mutate: jest.fn(),
isLoading: false,
error: null,
})),
useThirdPartyAuthHook: jest.fn(() => ({
mutate: jest.fn(),
isLoading: false,
error: null,
})),
}));
const secondaryProviders = {
id: 'saml-test_university',
name: 'Test University',
iconClass: 'fa-university',
iconImage: null,
loginUrl: '/auth/login/saml-test_university/?auth_entry=login&next=%2Fdashboard',
registerUrl: '/auth/login/saml-test_university/?auth_entry=register&next=%2Fdashboard',
};
// Mock the ThirdPartyAuthContext
const mockClearThirdPartyAuthErrorMessage = jest.fn();
jest.mock('../common-components/components/ThirdPartyAuthContext.tsx', () => ({
useThirdPartyAuthContext: jest.fn(() => ({
fieldDescriptions: {},
optionalFields: {
fields: {},
extended_profile: [],
},
thirdPartyAuthApiStatus: null,
thirdPartyAuthContext: {
autoSubmitRegForm: false,
currentProvider: null,
finishAuthUrl: null,
countryCode: null,
providers: [{
id: 'oa2-facebook',
name: 'Facebook',
iconClass: 'fa-facebook',
iconImage: null,
skipHintedLogin: false,
skipRegistrationForm: false,
loginUrl: '/auth/login/facebook-oauth2/?auth_entry=login&next=%2Fdashboard',
registerUrl: '/auth/login/facebook-oauth2/?auth_entry=register&next=%2Fdashboard',
}],
secondaryProviders: [{
id: 'saml-test',
name: 'Test University',
iconClass: 'fa-sign-in',
iconImage: null,
skipHintedLogin: false,
skipRegistrationForm: false,
loginUrl: '/auth/login/tpa-saml/?auth_entry=login&next=%2Fdashboard',
registerUrl: '/auth/login/tpa-saml/?auth_entry=register&next=%2Fdashboard',
}],
pipelineUserDetails: null,
errorMessage: null,
welcomePageRedirectUrl: null,
},
setThirdPartyAuthContextBegin: jest.fn(),
setThirdPartyAuthContextSuccess: jest.fn(),
setThirdPartyAuthContextFailure: jest.fn(),
clearThirdPartyAuthErrorMessage: mockClearThirdPartyAuthErrorMessage,
})),
ThirdPartyAuthProvider: ({ children }) => children,
}));
let queryClient;
const mockStore = configureStore();
describe('Logistration', () => {
const renderWrapper = (children) => {
queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: false,
let store = {};
const secondaryProviders = {
id: 'saml-test',
name: 'Test University',
loginUrl: '/dummy-auth',
registerUrl: '/dummy_auth',
};
const reduxWrapper = children => (
<IntlProvider locale="en">
<MemoryRouter>
<Provider store={store}>{children}</Provider>
</MemoryRouter>
</IntlProvider>
);
const initialState = {
register: {
registrationFormData: {
configurableFormFields: {
marketingEmailsOptIn: true,
},
mutations: {
retry: false,
formFields: {
name: '', email: '', username: '', password: '',
},
emailSuggestion: {
suggestion: '', type: '',
},
errors: {
name: '', email: '', username: '', password: '',
},
},
});
return (
<QueryClientProvider client={queryClient}>
<IntlProvider locale="en">
<MemoryRouter>
{children}
</MemoryRouter>
</IntlProvider>
</QueryClientProvider>
);
registrationResult: { success: false, redirectUrl: '' },
registrationError: {},
usernameSuggestions: [],
validationApiRateLimited: false,
},
commonComponents: {
thirdPartyAuthContext: {
providers: [],
secondaryProviders: [],
},
},
login: {
loginResult: { success: false, redirectUrl: '' },
},
};
beforeEach(() => {
// Avoid jest open handle error
jest.clearAllMocks();
mockNavigate.mockClear();
mockGetCsrfToken.mockClear();
store = mockStore(initialState);
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedUser: jest.fn(() => ({
userId: 3,
username: 'test-user',
})),
}));
// Configure i18n for testing
configure({
loggingService: { logError: jest.fn() },
config: {
@@ -169,25 +90,10 @@ describe('Logistration', () => {
},
messages: { 'es-419': {}, de: {}, 'en-us': {} },
});
// Set up default configuration for tests
mergeConfig({
DISABLE_ENTERPRISE_LOGIN: 'true',
ALLOW_PUBLIC_ACCOUNT_CREATION: 'true',
SHOW_REGISTRATION_LINKS: 'true',
TPA_HINT: '',
TPA_PROVIDER_ID: '',
THIRD_PARTY_AUTH_HINT: '',
PROVIDERS: [secondaryProviders],
SECONDARY_PROVIDERS: [secondaryProviders],
CURRENT_PROVIDER: null,
FINISHED_AUTH_PROVIDERS: [],
DISABLE_TPA_ON_FORM: false,
});
});
it('should do nothing when user clicks on the same tab (login/register) again', () => {
const { container } = render(renderWrapper(<Logistration selectedPage={REGISTER_PAGE} />));
const { container } = render(reduxWrapper(<Logistration />));
// While staying on the registration form, clicking the register tab again
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
@@ -199,14 +105,14 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: true,
});
const { container } = render(renderWrapper(<Logistration />));
const { container } = render(reduxWrapper(<Logistration />));
expect(container.querySelector('RegistrationPage')).toBeDefined();
});
it('should render login page', () => {
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(renderWrapper(<Logistration {...props} />));
const { container } = render(reduxWrapper(<Logistration {...props} />));
expect(container.querySelector('LoginPage')).toBeDefined();
});
@@ -217,18 +123,18 @@ describe('Logistration', () => {
});
let props = { selectedPage: LOGIN_PAGE };
const { rerender } = render(renderWrapper(<Logistration {...props} />));
const { rerender } = render(reduxWrapper(<Logistration {...props} />));
// verifying sign in tab
expect(screen.getByRole('tab', { name: 'Sign in' })).toBeDefined();
// verifying sign in heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
// register page is still accessible when SHOW_REGISTRATION_LINKS is false
// but it needs to be accessed directly
props = { selectedPage: REGISTER_PAGE };
rerender(renderWrapper(<Logistration {...props} />));
rerender(reduxWrapper(<Logistration {...props} />));
// verifying register button
expect(screen.getByRole('button', { name: 'Create an account for free' })).toBeDefined();
// verifying register heading
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Register');
});
it('should render only login page when public account creation is disabled', () => {
@@ -238,11 +144,24 @@ describe('Logistration', () => {
SHOW_REGISTRATION_LINKS: 'true',
});
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(renderWrapper(<Logistration {...props} />));
store = mockStore({
...initialState,
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
// verifying sign in tab for institution login false
expect(screen.getByRole('tab', { name: 'Sign in' })).toBeDefined();
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(reduxWrapper(<Logistration {...props} />));
// verifying sign in heading for institution login false
expect(screen.getByRole('heading', { level: 3 }).textContent).toEqual('Sign in');
// verifying tabs heading for institution login true
fireEvent.click(screen.getByRole('link'));
@@ -255,8 +174,21 @@ describe('Logistration', () => {
ALLOW_PUBLIC_ACCOUNT_CREATION: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
render(renderWrapper(<Logistration {...props} />));
render(reduxWrapper(<Logistration {...props} />));
expect(screen.getByText('Institution/campus credentials')).toBeDefined();
// on clicking "Institution/campus credentials" button, it should display institution login page
@@ -273,8 +205,21 @@ describe('Logistration', () => {
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
const props = { selectedPage: LOGIN_PAGE };
render(renderWrapper(<Logistration {...props} />));
render(reduxWrapper(<Logistration {...props} />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
@@ -290,10 +235,23 @@ describe('Logistration', () => {
DISABLE_ENTERPRISE_LOGIN: 'true',
});
store = mockStore({
...initialState,
commonComponents: {
thirdPartyAuthContext: {
currentProvider: null,
finishAuthUrl: null,
providers: [],
secondaryProviders: [secondaryProviders],
},
thirdPartyAuthApiStatus: COMPLETE_STATE,
},
});
delete window.location;
window.location = { hostname: getConfig().SITE_NAME, href: getConfig().BASE_URL };
render(renderWrapper(<Logistration />));
render(reduxWrapper(<Logistration />));
fireEvent.click(screen.getByText('Institution/campus credentials'));
expect(screen.getByText('Test University')).toBeDefined();
@@ -302,52 +260,25 @@ describe('Logistration', () => {
});
});
it('should switch to login tab when login tab is clicked', () => {
const { container } = render(renderWrapper(<Logistration />));
it('should fire action to backup registration form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
// Verify the tab switch occurred - check for active login tab
expect(container.querySelector('a[data-rb-event-key="/login"].active')).toBeTruthy();
expect(store.dispatch).toHaveBeenCalledWith(backupRegistrationForm());
});
it('should switch to register tab when register tab is clicked', () => {
it('should fire action to backup login form on tab click', () => {
store.dispatch = jest.fn(store.dispatch);
const props = { selectedPage: LOGIN_PAGE };
const { container } = render(renderWrapper(<Logistration {...props} />));
const { container } = render(reduxWrapper(<Logistration {...props} />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/register"]'));
// Verify the tab switch occurred - check for active register tab
expect(container.querySelector('a[data-rb-event-key="/register"].active')).toBeTruthy();
expect(store.dispatch).toHaveBeenCalledWith(backupLoginForm());
});
it('should clear tpa context errorMessage tab click', () => {
const { container } = render(renderWrapper(<Logistration />));
store.dispatch = jest.fn(store.dispatch);
const { container } = render(reduxWrapper(<Logistration />));
fireEvent.click(container.querySelector('a[data-rb-event-key="/login"]'));
expect(mockClearThirdPartyAuthErrorMessage).toHaveBeenCalled();
});
it('should call authService getCsrfTokenService on component mount', () => {
render(renderWrapper(<Logistration selectedPage={LOGIN_PAGE} />));
expect(mockGetCsrfToken).toHaveBeenCalledWith(getConfig().LMS_BASE_URL);
});
it('should send correct page events for login and register when handling institution login', () => {
render(renderWrapper(<Logistration selectedPage={LOGIN_PAGE} />));
const institutionButton = screen.getByText('Institution/campus credentials');
fireEvent.click(institutionButton);
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
const { container: registerContainer } = render(renderWrapper(<Logistration selectedPage={REGISTER_PAGE} />));
const registerInstitutionButton = registerContainer.querySelector('#institution-login');
if (registerInstitutionButton) {
fireEvent.click(registerInstitutionButton);
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
}
});
it('should handle institution login with string parameters correctly', () => {
render(renderWrapper(<Logistration selectedPage={LOGIN_PAGE} />));
const institutionButton = screen.getByText('Institution/campus credentials');
sendPageEvent.mockClear();
fireEvent.click(institutionButton);
expect(sendTrackEvent).toHaveBeenCalledWith('edx.bi.institution_login_form.toggled', { category: 'user-engagement' });
expect(sendPageEvent).toHaveBeenCalledWith('login_and_registration', 'institution_login');
expect(store.dispatch).toHaveBeenCalledWith(clearThirdPartyAuthContextErrorMessage());
});
});

View File

@@ -1,47 +0,0 @@
# Login Component Slot
### Slot ID: `org.openedx.frontend.authn.login_component.v1`
## Description
This slot is used to replace/modify/hide the login component.
## Example
### Default content
![Default Login Page](./default_component.png)
### With a prepended message
![Login Page with ](./component_with_prefix.png)
The following `env.config.jsx` will add a message before the login component.
```js
import { DIRECT_PLUGIN, PLUGIN_OPERATIONS } from '@openedx/frontend-plugin-framework';
// Load environment variables from .env file
const config = {
...process.env,
pluginSlots: {
'org.openedx.frontend.authn.login_component.v1': {
keepDefault: true,
plugins: [
{
op: PLUGIN_OPERATIONS.Insert,
widget: {
id: 'test_plugin',
type: DIRECT_PLUGIN,
priority: 1,
RenderWidget: () => (
<h2>You're logging into TEST Instance.</h2>
)
},
},
],
},
},
};
export default config;
```

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.5 KiB

View File

@@ -1,29 +0,0 @@
import { PluginSlot } from '@openedx/frontend-plugin-framework';
import PropTypes from 'prop-types';
import LoginPage from '../../login/LoginPage';
const LoginComponentSlot = ({
institutionLogin,
handleInstitutionLogin,
}) => (
<PluginSlot
id="org.openedx.frontend.authn.login_component.v1"
pluginProps={{
isInstitutionLogin: institutionLogin,
setInstitutionLogin: handleInstitutionLogin,
}}
>
<LoginPage
institutionLogin={institutionLogin}
handleInstitutionLogin={handleInstitutionLogin}
/>
</PluginSlot>
);
LoginComponentSlot.propTypes = {
institutionLogin: PropTypes.bool,
handleInstitutionLogin: PropTypes.func,
};
export default LoginComponentSlot;

View File

@@ -1,4 +1,5 @@
import { useEffect, useState } from 'react';
import React, { useEffect, useState } from 'react';
import { connect } from 'react-redux';
import { getConfig, snakeCaseObject } from '@edx/frontend-platform';
import { identifyAuthenticatedUser, sendPageEvent, sendTrackEvent } from '@edx/frontend-platform/analytics';
@@ -17,21 +18,21 @@ import {
StatefulButton,
} from '@openedx/paragon';
import { Error } from '@openedx/paragon/icons';
import PropTypes from 'prop-types';
import { Helmet } from 'react-helmet';
import { useLocation } from 'react-router-dom';
import { ProgressiveProfilingProvider, useProgressiveProfilingContext } from './components/ProgressiveProfilingContext';
import { saveUserProfile } from './data/actions';
import { welcomePageContextSelector } from './data/selectors';
import messages from './messages';
import ProgressiveProfilingPageModal from './ProgressiveProfilingPageModal';
import BaseContainer from '../base-container';
import { RedirectLogistration } from '../common-components';
import { useSaveUserProfile } from './data/apiHook';
import { ThirdPartyAuthProvider, useThirdPartyAuthContext } from '../common-components/components/ThirdPartyAuthContext';
import { useThirdPartyAuthHook } from '../common-components/data/apiHook';
import { getThirdPartyAuthContext } from '../common-components/data/actions';
import {
AUTHN_PROGRESSIVE_PROFILING,
COMPLETE_STATE,
DEFAULT_REDIRECT_URL,
DEFAULT_STATE,
FAILURE_STATE,
PENDING_STATE,
} from '../data/constants';
@@ -39,26 +40,15 @@ import isOneTrustFunctionalCookieEnabled from '../data/oneTrust';
import { getAllPossibleQueryParams, isHostAvailableInQueryParams } from '../data/utils';
import { FormFieldRenderer } from '../field-renderer';
const ProgressiveProfilingInner = () => {
const ProgressiveProfiling = (props) => {
const { formatMessage } = useIntl();
const {
thirdPartyAuthApiStatus,
setThirdPartyAuthContextSuccess,
setThirdPartyAuthContextFailure,
optionalFields,
} = useThirdPartyAuthContext();
const welcomePageContext = optionalFields;
const {
getFieldDataFromBackend,
submitState,
showError,
success,
} = useProgressiveProfilingContext();
// Hook for saving user profile
const saveUserProfileMutation = useSaveUserProfile();
welcomePageContext,
welcomePageContextApiStatus,
} = props;
const location = useLocation();
const registrationEmbedded = isHostAvailableInQueryParams();
@@ -75,40 +65,27 @@ const ProgressiveProfilingInner = () => {
const [showModal, setShowModal] = useState(false);
const [showRecommendationsPage, setShowRecommendationsPage] = useState(false);
const { data, isSuccess, error } = useThirdPartyAuthHook(AUTHN_PROGRESSIVE_PROFILING,
{ is_welcome_page: true, next: queryParams?.next });
useEffect(() => {
if (registrationEmbedded) {
if (isSuccess && data) {
setThirdPartyAuthContextSuccess(
data.fieldDescriptions,
data.optionalFields,
data.thirdPartyAuthContext,
);
}
if (error) {
setThirdPartyAuthContextFailure();
}
getFieldDataFromBackend({ is_welcome_page: true, next: queryParams?.next });
} else {
configureAuth(AxiosJwtAuthService, { loggingService: getLoggingService(), config: getConfig() });
}
}, [registrationEmbedded, queryParams?.next, isSuccess, data, error,
setThirdPartyAuthContextSuccess, setThirdPartyAuthContextFailure]);
}, [registrationEmbedded, getFieldDataFromBackend, queryParams?.next]);
useEffect(() => {
const registrationResponse = location.state?.registrationResult;
if (registrationResponse) {
setRegistrationResult(registrationResponse);
setFormFieldData({
fields: location.state?.optionalFields.fields || {},
extendedProfile: location.state?.optionalFields.extended_profile || [],
fields: location.state?.optionalFields.fields,
extendedProfile: location.state?.optionalFields.extended_profile,
});
}
}, [location.state?.registrationResult, location.state?.optionalFields]);
}, [location.state]);
useEffect(() => {
if (registrationEmbedded && welcomePageContext && Object.keys(welcomePageContext).includes('fields')) {
if (registrationEmbedded && Object.keys(welcomePageContext).includes('fields')) {
setFormFieldData({
fields: welcomePageContext.fields,
extendedProfile: welcomePageContext.extended_profile,
@@ -151,8 +128,8 @@ const ProgressiveProfilingInner = () => {
if (
!authenticatedUser
|| !(location.state?.registrationResult || registrationEmbedded)
|| thirdPartyAuthApiStatus === FAILURE_STATE
|| (thirdPartyAuthApiStatus === COMPLETE_STATE && !Object.keys(welcomePageContext).includes('fields'))
|| welcomePageContextApiStatus === FAILURE_STATE
|| (welcomePageContextApiStatus === COMPLETE_STATE && !Object.keys(welcomePageContext).includes('fields'))
) {
const DASHBOARD_URL = getConfig().LMS_BASE_URL.concat(DEFAULT_REDIRECT_URL);
global.location.assign(DASHBOARD_URL);
@@ -171,7 +148,7 @@ const ProgressiveProfilingInner = () => {
delete payload[fieldName];
});
}
saveUserProfileMutation.mutate({ username: authenticatedUser.username, data: snakeCaseObject(payload) });
props.saveUserProfile(authenticatedUser.username, snakeCaseObject(payload));
sendTrackEvent(
'edx.bi.welcome.page.submit.clicked',
@@ -218,7 +195,6 @@ const ProgressiveProfilingInner = () => {
);
});
const shouldRedirect = success;
return (
<BaseContainer showWelcomeBanner fullName={authenticatedUser?.fullName || authenticatedUser?.name}>
<Helmet>
@@ -227,13 +203,13 @@ const ProgressiveProfilingInner = () => {
</title>
</Helmet>
<ProgressiveProfilingPageModal isOpen={showModal} redirectUrl={registrationResult.redirectUrl} />
{(shouldRedirect && welcomePageContext.nextUrl) && (
{(props.shouldRedirect && welcomePageContext.nextUrl) && (
<RedirectLogistration
success
redirectUrl={registrationResult.redirectUrl}
/>
)}
{shouldRedirect && (
{props.shouldRedirect && (
<RedirectLogistration
success
redirectUrl={registrationResult.redirectUrl}
@@ -243,7 +219,7 @@ const ProgressiveProfilingInner = () => {
/>
)}
<div className="mw-xs m-4 pp-page-content">
{registrationEmbedded && thirdPartyAuthApiStatus === PENDING_STATE ? (
{registrationEmbedded && welcomePageContextApiStatus === PENDING_STATE ? (
<Spinner animation="border" variant="primary" id="tpa-spinner" />
) : (
<>
@@ -305,12 +281,51 @@ const ProgressiveProfilingInner = () => {
);
};
const ProgressiveProfiling = (props) => (
<ThirdPartyAuthProvider>
<ProgressiveProfilingProvider>
<ProgressiveProfilingInner {...props} />
</ProgressiveProfilingProvider>
</ThirdPartyAuthProvider>
);
ProgressiveProfiling.propTypes = {
authenticatedUser: PropTypes.shape({
username: PropTypes.string,
userId: PropTypes.number,
fullName: PropTypes.string,
}),
showError: PropTypes.bool,
shouldRedirect: PropTypes.bool,
submitState: PropTypes.string,
welcomePageContext: PropTypes.shape({
extended_profile: PropTypes.arrayOf(PropTypes.string),
fields: PropTypes.shape({}),
nextUrl: PropTypes.string,
}),
welcomePageContextApiStatus: PropTypes.string,
// Actions
getFieldDataFromBackend: PropTypes.func.isRequired,
saveUserProfile: PropTypes.func.isRequired,
};
export default ProgressiveProfiling;
ProgressiveProfiling.defaultProps = {
authenticatedUser: {},
shouldRedirect: false,
showError: false,
submitState: DEFAULT_STATE,
welcomePageContext: {},
welcomePageContextApiStatus: PENDING_STATE,
};
const mapStateToProps = state => {
const welcomePageStore = state.welcomePage;
return {
shouldRedirect: welcomePageStore.success,
showError: welcomePageStore.showError,
submitState: welcomePageStore.submitState,
welcomePageContext: welcomePageContextSelector(state),
welcomePageContextApiStatus: state.commonComponents.thirdPartyAuthApiStatus,
};
};
export default connect(
mapStateToProps,
{
saveUserProfile,
getFieldDataFromBackend: getThirdPartyAuthContext,
},
)(ProgressiveProfiling);

View File

@@ -1,80 +0,0 @@
import { createContext, FC, ReactNode, useContext, useMemo, useState, useCallback } from 'react';
import {
DEFAULT_STATE,
} from '../../data/constants';
interface ProgressiveProfilingContextType {
isLoading: boolean;
showError: boolean;
success: boolean;
submitState?: string;
setLoading: (loading: boolean) => void;
setShowError: (showError: boolean) => void;
setSuccess: (success: boolean) => void;
setSubmitState: (state: string) => void;
clearState: () => void;
}
const ProgressiveProfilingContext = createContext<ProgressiveProfilingContextType | undefined>(undefined);
interface ProgressiveProfilingProviderProps {
children: ReactNode;
}
export const ProgressiveProfilingProvider: FC<ProgressiveProfilingProviderProps> = ({ children }) => {
const [isLoading, setIsLoading] = useState(false);
const [showError, setShowError] = useState(false);
const [success, setSuccess] = useState(false);
const [submitState, setSubmitState] = useState<string>(DEFAULT_STATE);
const setLoading = useCallback((loading: boolean) => {
setIsLoading(loading);
if (loading) {
setShowError(false);
setSuccess(false);
}
}, []);
const clearState = useCallback(() => {
setIsLoading(false);
setShowError(false);
setSuccess(false);
}, []);
const value = useMemo(() => ({
isLoading,
showError,
success,
setLoading,
setShowError,
setSuccess,
clearState,
submitState,
setSubmitState,
}), [
isLoading,
showError,
success,
setLoading,
setShowError,
setSuccess,
clearState,
submitState,
setSubmitState,
]);
return (
<ProgressiveProfilingContext.Provider value={value}>
{children}
</ProgressiveProfilingContext.Provider>
);
};
export const useProgressiveProfilingContext = (): ProgressiveProfilingContextType => {
const context = useContext(ProgressiveProfilingContext);
if (context === undefined) {
throw new Error('useProgressiveProfilingContext must be used within a ProgressiveProfilingProvider');
}
return context;
};

View File

@@ -0,0 +1,22 @@
import { AsyncActionType } from '../../data/utils';
export const GET_FIELDS_DATA = new AsyncActionType('FIELD_DESCRIPTION', 'GET_FIELDS_DATA');
export const SAVE_USER_PROFILE = new AsyncActionType('USER_PROFILE', 'SAVE_USER_PROFILE');
// save additional user information
export const saveUserProfile = (username, data) => ({
type: SAVE_USER_PROFILE.BASE,
payload: { username, data },
});
export const saveUserProfileBegin = () => ({
type: SAVE_USER_PROFILE.BEGIN,
});
export const saveUserProfileSuccess = () => ({
type: SAVE_USER_PROFILE.SUCCESS,
});
export const saveUserProfileFailure = () => ({
type: SAVE_USER_PROFILE.FAILURE,
});

View File

@@ -1,169 +0,0 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
import { patchAccount } from './api';
// Mock the platform dependencies
jest.mock('@edx/frontend-platform', () => ({
getConfig: jest.fn(),
}));
jest.mock('@edx/frontend-platform/auth', () => ({
getAuthenticatedHttpClient: jest.fn(),
}));
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>;
const mockGetAuthenticatedHttpClient = getAuthenticatedHttpClient as jest.MockedFunction<typeof getAuthenticatedHttpClient>;
describe('progressive-profiling api', () => {
const mockHttpClient = {
patch: jest.fn(),
};
const mockConfig = {
LMS_BASE_URL: 'http://localhost:18000',
};
beforeEach(() => {
jest.clearAllMocks();
mockGetConfig.mockReturnValue(mockConfig);
mockGetAuthenticatedHttpClient.mockReturnValue(mockHttpClient as any);
});
describe('patchAccount', () => {
const mockUsername = 'testuser123';
const mockCommitValues = {
gender: 'm',
extended_profile: [
{ field_name: 'company', field_value: 'Test Company' },
{ field_name: 'level_of_education', field_value: 'Bachelor\'s Degree' }
]
};
const expectedUrl = `${mockConfig.LMS_BASE_URL}/api/user/v1/accounts/${mockUsername}`;
const expectedConfig = {
headers: { 'Content-Type': 'application/merge-patch+json' },
};
it('should patch user account successfully', async () => {
const mockResponse = { data: { success: true } };
mockHttpClient.patch.mockResolvedValueOnce(mockResponse);
await patchAccount(mockUsername, mockCommitValues);
expect(mockGetAuthenticatedHttpClient).toHaveBeenCalled();
expect(mockHttpClient.patch).toHaveBeenCalledWith(
expectedUrl,
mockCommitValues,
expectedConfig
);
});
it('should handle mixed profile and extended profile updates', async () => {
const mixedCommitValues = {
gender: 'o',
year_of_birth: 1985,
extended_profile: [
{ field_name: 'level_of_education', field_value: 'Master\'s Degree' }
]
};
const mockResponse = { data: { success: true } };
mockHttpClient.patch.mockResolvedValueOnce(mockResponse);
await patchAccount(mockUsername, mixedCommitValues);
expect(mockHttpClient.patch).toHaveBeenCalledWith(
expectedUrl,
mixedCommitValues,
expectedConfig
);
});
it('should handle empty commit values', async () => {
const emptyCommitValues = {};
const mockResponse = { data: { success: true } };
mockHttpClient.patch.mockResolvedValueOnce(mockResponse);
await patchAccount(mockUsername, emptyCommitValues);
expect(mockHttpClient.patch).toHaveBeenCalledWith(
expectedUrl,
emptyCommitValues,
expectedConfig
);
});
it('should construct correct URL with username', async () => {
const differentUsername = 'anotheruser456';
const mockResponse = { data: { success: true } };
mockHttpClient.patch.mockResolvedValueOnce(mockResponse);
await patchAccount(differentUsername, mockCommitValues);
expect(mockHttpClient.patch).toHaveBeenCalledWith(
`${mockConfig.LMS_BASE_URL}/api/user/v1/accounts/${differentUsername}`,
mockCommitValues,
expectedConfig
);
});
it('should throw error when API call fails', async () => {
const mockError = new Error('API Error: Account update failed');
mockHttpClient.patch.mockRejectedValueOnce(mockError);
await expect(patchAccount(mockUsername, mockCommitValues)).rejects.toThrow('API Error: Account update failed');
expect(mockHttpClient.patch).toHaveBeenCalledWith(
expectedUrl,
mockCommitValues,
expectedConfig
);
});
it('should handle HTTP 400 error', async () => {
const mockError = {
response: {
status: 400,
data: {
field_errors: {
gender: 'Invalid gender value'
}
}
},
message: 'Bad Request'
};
mockHttpClient.patch.mockRejectedValueOnce(mockError);
await expect(patchAccount(mockUsername, mockCommitValues)).rejects.toEqual(mockError);
});
it('should handle network errors', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockHttpClient.patch.mockRejectedValueOnce(networkError);
await expect(patchAccount(mockUsername, mockCommitValues)).rejects.toThrow('Network Error');
});
it('should handle timeout errors', async () => {
const timeoutError = new Error('Request timeout');
timeoutError.name = 'TimeoutError';
mockHttpClient.patch.mockRejectedValueOnce(timeoutError);
await expect(patchAccount(mockUsername, mockCommitValues)).rejects.toThrow('Request timeout');
});
it('should handle null or undefined username gracefully', async () => {
const mockResponse = { data: { success: true } };
mockHttpClient.patch.mockResolvedValueOnce(mockResponse);
await patchAccount(null, mockCommitValues);
expect(mockHttpClient.patch).toHaveBeenCalledWith(
`${mockConfig.LMS_BASE_URL}/api/user/v1/accounts/null`,
mockCommitValues,
expectedConfig
);
});
});
});

View File

@@ -1,232 +0,0 @@
import React from 'react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { renderHook, waitFor } from '@testing-library/react';
import * as api from './api';
import { useSaveUserProfile } from './apiHook';
import { useProgressiveProfilingContext } from '../components/ProgressiveProfilingContext';
import { COMPLETE_STATE, DEFAULT_STATE } from '../../data/constants';
// Mock the API function
jest.mock('./api', () => ({
patchAccount: jest.fn(),
}));
// Mock the progressive profiling context
jest.mock('../components/ProgressiveProfilingContext', () => ({
useProgressiveProfilingContext: jest.fn(),
}));
const mockPatchAccount = api.patchAccount as jest.MockedFunction<typeof api.patchAccount>;
const mockUseProgressiveProfilingContext = useProgressiveProfilingContext as jest.MockedFunction<typeof useProgressiveProfilingContext>;
// Test wrapper component
const createWrapper = () => {
const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});
return function TestWrapper({ children }: { children: React.ReactNode }) {
return React.createElement(QueryClientProvider, { client: queryClient }, children);
};
};
describe('useSaveUserProfile', () => {
const mockSetShowError = jest.fn();
const mockSetSuccess = jest.fn();
const mockSetSubmitState = jest.fn();
const mockContextValue = {
setShowError: mockSetShowError,
setSuccess: mockSetSuccess,
setSubmitState: mockSetSubmitState,
};
beforeEach(() => {
jest.clearAllMocks();
mockUseProgressiveProfilingContext.mockReturnValue(mockContextValue);
});
it('should initialize with default state', () => {
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
expect(result.current.isPending).toBe(false);
expect(result.current.isError).toBe(false);
expect(result.current.isSuccess).toBe(false);
expect(result.current.error).toBe(null);
});
it('should save user profile successfully', async () => {
const mockPayload = {
username: 'testuser123',
data: {
gender: 'm',
extended_profile: [
{ field_name: 'company', field_value: 'Test Company' },
],
},
};
const mockResponse = { success: true };
mockPatchAccount.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
// Check API was called correctly
expect(mockPatchAccount).toHaveBeenCalledWith(mockPayload.username, mockPayload.data);
// Check success state is set
expect(mockSetSuccess).toHaveBeenCalledWith(true);
expect(mockSetSubmitState).toHaveBeenCalledWith(COMPLETE_STATE);
});
it('should handle API error and set error state', async () => {
const mockPayload = {
username: 'testuser123',
data: { gender: 'm' },
};
const mockError = new Error('Failed to save profile');
mockPatchAccount.mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Check API was called
expect(mockPatchAccount).toHaveBeenCalledWith(mockPayload.username, mockPayload.data);
// Check error state is set
expect(mockSetSubmitState).toHaveBeenCalledWith(DEFAULT_STATE);
expect(result.current.error).toEqual(mockError);
});
it('should handle non-Error objects and set generic error message', async () => {
const mockPayload = {
username: 'testuser123',
data: { gender: 'm' },
};
const mockError = { message: 'Something went wrong', status: 500 };
mockPatchAccount.mockRejectedValueOnce(mockError);
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
// Check error state is set
expect(mockSetSubmitState).toHaveBeenCalledWith(DEFAULT_STATE);
});
it('should properly handle extended_profile data structure', async () => {
const mockPayload = {
username: 'testuser123',
data: {
gender: 'f',
extended_profile: [
{ field_name: 'company', field_value: 'Acme Corp' },
{ field_name: 'level_of_education', field_value: 'Bachelor\'s Degree' },
],
},
};
const mockResponse = { success: true, updated_fields: ['gender', 'extended_profile'] };
mockPatchAccount.mockResolvedValueOnce(mockResponse);
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockPatchAccount).toHaveBeenCalledWith(mockPayload.username, mockPayload.data);
expect(mockSetSuccess).toHaveBeenCalledWith(true);
});
it('should handle network errors gracefully', async () => {
const mockPayload = {
username: 'testuser123',
data: { gender: 'm' },
};
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockPatchAccount.mockRejectedValueOnce(networkError);
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isError).toBe(true);
});
expect(mockSetSubmitState).toHaveBeenCalledWith(DEFAULT_STATE);
});
it('should reset states correctly on each mutation attempt', async () => {
const mockPayload = {
username: 'testuser123',
data: { gender: 'm' },
};
mockPatchAccount.mockResolvedValueOnce({ success: true });
const { result } = renderHook(() => useSaveUserProfile(), {
wrapper: createWrapper(),
});
// First mutation
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockSetSuccess).toHaveBeenCalledWith(true);
jest.clearAllMocks();
mockPatchAccount.mockResolvedValueOnce({ success: true });
// Second mutation
result.current.mutate(mockPayload);
await waitFor(() => {
expect(result.current.isSuccess).toBe(true);
});
expect(mockSetSuccess).toHaveBeenCalledWith(true);
});
});

View File

@@ -1,43 +0,0 @@
import { useMutation } from '@tanstack/react-query';
import { patchAccount } from './api';
import {
COMPLETE_STATE, DEFAULT_STATE,
} from '../../data/constants';
import { useProgressiveProfilingContext } from '../components/ProgressiveProfilingContext';
interface SaveUserProfilePayload {
username: string;
data: Record<string, any>;
}
interface UseSaveUserProfileOptions {
onSuccess?: () => void;
onError?: (error: unknown) => void;
}
const useSaveUserProfile = (options: UseSaveUserProfileOptions = {}) => {
const { setSuccess, setSubmitState } = useProgressiveProfilingContext();
return useMutation({
mutationFn: async ({ username, data }: SaveUserProfilePayload) => (
patchAccount(username, data)
),
onSuccess: () => {
setSuccess(true);
setSubmitState(COMPLETE_STATE);
if (options.onSuccess) {
options.onSuccess();
}
},
onError: (error: unknown) => {
setSubmitState(DEFAULT_STATE);
if (options.onError) {
options.onError(error);
}
},
});
};
export {
useSaveUserProfile,
};

View File

@@ -0,0 +1,38 @@
import { SAVE_USER_PROFILE } from './actions';
import {
DEFAULT_STATE, PENDING_STATE,
} from '../../data/constants';
export const defaultState = {
extendedProfile: [],
fieldDescriptions: {},
success: false,
submitState: DEFAULT_STATE,
showError: false,
};
const reducer = (state = defaultState, action = {}) => {
switch (action.type) {
case SAVE_USER_PROFILE.BEGIN:
return {
...state,
submitState: PENDING_STATE,
};
case SAVE_USER_PROFILE.SUCCESS:
return {
...state,
success: true,
showError: false,
};
case SAVE_USER_PROFILE.FAILURE:
return {
...state,
submitState: DEFAULT_STATE,
showError: true,
};
default:
return state;
}
};
export default reducer;

View File

@@ -0,0 +1,24 @@
import { call, put, takeEvery } from 'redux-saga/effects';
import {
SAVE_USER_PROFILE,
saveUserProfileBegin,
saveUserProfileFailure,
saveUserProfileSuccess,
} from './actions';
import { patchAccount } from './service';
export function* saveUserProfileInformation(action) {
try {
yield put(saveUserProfileBegin());
yield call(patchAccount, action.payload.username, action.payload.data);
yield put(saveUserProfileSuccess());
} catch (e) {
yield put(saveUserProfileFailure());
}
}
export default function* saga() {
yield takeEvery(SAVE_USER_PROFILE.BASE, saveUserProfileInformation);
}

View File

@@ -0,0 +1,14 @@
import { createSelector } from 'reselect';
export const storeName = 'commonComponents';
export const commonComponentsSelector = state => ({ ...state[storeName] });
export const welcomePageContextSelector = createSelector(
commonComponentsSelector,
commonComponents => ({
fields: commonComponents.optionalFields.fields,
extended_profile: commonComponents.optionalFields.extended_profile,
nextUrl: commonComponents.thirdPartyAuthContext.welcomePageRedirectUrl,
}),
);

View File

@@ -1,7 +1,8 @@
import { getConfig } from '@edx/frontend-platform';
import { getAuthenticatedHttpClient } from '@edx/frontend-platform/auth';
const patchAccount = async (username, commitValues) => {
// eslint-disable-next-line import/prefer-default-export
export async function patchAccount(username, commitValues) {
const requestConfig = {
headers: { 'Content-Type': 'application/merge-patch+json' },
};
@@ -15,8 +16,4 @@ const patchAccount = async (username, commitValues) => {
.catch((error) => {
throw (error);
});
};
export {
patchAccount,
};
}

View File

@@ -1,2 +1,5 @@
export const storeName = 'welcomePage';
export { default as ProgressiveProfiling } from './ProgressiveProfiling';
export { default as reducer } from './data/reducers';
export { default as saga } from './data/sagas';

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