diff --git a/..env.development.swp b/..env.development.swp new file mode 100644 index 00000000..0b77c06f Binary files /dev/null and b/..env.development.swp differ diff --git a/.env.development b/.env.development index fc33a91e..3d17bdfe 100644 --- a/.env.development +++ b/.env.development @@ -1,39 +1,31 @@ -APP_ID='enterprise-checkout' -NODE_ENV='development' -PORT=1989 -ACCESS_TOKEN_COOKIE_NAME='edx-jwt-cookie-header-payload' -BASE_URL='http://localhost:1989' -ENTERPRISE_ACCESS_BASE_URL='http://localhost:18270' -CREDENTIALS_BASE_URL='http://localhost:18150' -CSRF_TOKEN_API_PATH='/csrf/api/v1/token' -ECOMMERCE_BASE_URL='http://localhost:18130' -LANGUAGE_PREFERENCE_COOKIE_NAME='openedx-language-preference' -LMS_BASE_URL='http://localhost:18000' -LOGIN_URL='http://localhost:18000/login' -LOGOUT_URL='http://localhost:18000/logout' -LOGO_URL=https://edx-cdn.org/v3/default/logo.svg -LOGO_TRADEMARK_URL=https://edx-cdn.org/v3/default/logo-trademark.svg -LOGO_WHITE_URL=https://edx-cdn.org/v3/default/logo-white.svg -FAVICON_URL=https://edx-cdn.org/v3/default/favicon.ico -MARKETING_SITE_BASE_URL='http://localhost:18000' -ORDER_HISTORY_URL='http://localhost:1996/orders' -REFRESH_ACCESS_TOKEN_ENDPOINT='http://localhost:18000/login_refresh' -SEGMENT_KEY='' -SITE_NAME=localhost -USER_INFO_COOKIE_NAME='edx-user-info' -MFE_CONFIG_API_URL='' -ENABLE_NEW_RELIC='false' -PARAGON_THEMES_URLS={} -PUBLISHABLE_STRIPE_API_KEY='' -TERMS_OF_SERVICE_URL='https://edx.org/edx-terms-service' -PRIVACY_POLICY_URL='https://edx.org/edx-privacy-policy' -ENTERPRISE_PRODUCT_DESCRIPTIONS_AND_TERMS_URL='https://business.edx.org/product-descriptions-and-terms/' -ENTERPRISE_SALES_TERMS_AND_CONDITIONS_URL='https://business.edx.org/enterprise-sales-terms/' -COMPARE_ENTERPRISE_PLANS_URL='' -CONTACT_SUPPORT_URL='' -RECAPTCHA_SITE_KEY_WEB='' -FEATURE_SELF_SERVICE_PURCHASING='' -FEATURE_SELF_SERVICE_PURCHASING_KEY='' -FEATURE_SELF_SERVICE_ESSENTIALS= '' -FEATURE_SELF_SERVICE_ESSENTIALS_KEY='' -FEATURE_SELF_SERVICE_SITE_KEY= '' +APP_ID=enterprise-checkout +NODE_ENV=development +PORT=2012 + +# Frontend URL (Codespaces or dev tunnel URL) +BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-2012.app.github.dev + +ACCESS_TOKEN_COOKIE_NAME=edx-jwt-cookie-header-payload +USER_INFO_COOKIE_NAME=edx-user-info + +# LMS +LMS_BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-18000.app.github.dev + +# APIs (Update these to tunnel URLs or the Codespace forwarded ports) +DISCOVERY_API_BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-18381.app.github.dev +ENTERPRISE_CATALOG_API_BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-18160.app.github.dev +ENTERPRISE_ACCESS_BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-18270.app.github.dev +ECOMMERCE_BASE_URL=https://vigilant-cod-r49rvwprq4p4f596-18130.app.github.dev + +# Auth (Disabled for devstack) +LOGIN_URL= +LOGOUT_URL= +REFRESH_ACCESS_TOKEN_ENDPOINT= + +# Cookies +LANGUAGE_PREFERENCE_COOKIE_NAME=openedx-language-preference + +CSRF_TOKEN_API_PATH=/csrf/api/v1/token +SITE_NAME=vigilant-cod-r49rvwprq4p4f596-2012.app.github.dev + +ENABLE_NEW_RELIC=false \ No newline at end of file diff --git a/jest.config.js b/jest.config.js index a0e696e5..606a68c3 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,25 +1,24 @@ +// jest.config.js const { createConfig } = require('@openedx/frontend-build'); -const {pathsToModuleNameMapper} = require("ts-jest"); -const { compilerOptions } = require("./tsconfig.json"); +const { pathsToModuleNameMapper } = require('ts-jest'); +const { compilerOptions } = require('./tsconfig.json'); -process.env.TZ='UTC' +process.env.TZ = 'UTC'; const config = createConfig('jest', { - // setupFilesAfterEnv is used after the jest environment has been loaded. In general this is what you want. - // If you want to add config BEFORE jest loads, use setupFiles instead. setupFilesAfterEnv: [ - '/src/setupTest.ts', + '/src/setupTest.ts' ], moduleNameMapper: pathsToModuleNameMapper(compilerOptions.paths, { - prefix: '/', + prefix: '/' }), moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json'], coveragePathIgnorePatterns: [ 'src/setupTest.ts', - 'src/i18n', - ], + 'src/i18n' + ] }); config.transformIgnorePatterns = ['node_modules/(?!(lodash-es|@(open)?edx)/)']; -module.exports = config; +module.exports = config; \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 7caf79af..60e18db8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,13 +49,15 @@ "@openedx/frontend-build": "^14.6.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^14.1.2", + "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.1.4", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "rosie": "^2.1.1", - "ts-jest": "^29.4.0" + "ts-jest": "^29.4.0", + "whatwg-fetch": "^3.6.20" } }, "node_modules/@adobe/css-tools": { @@ -2592,6 +2594,40 @@ "typescript": "^4.9.4" } }, + "node_modules/@emnapi/core": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.8.1.tgz", + "integrity": "sha512-AvT9QFpxK0Zd8J0jopedNm+w/2fIzvtPKPjqyw9jwvBaReTTqPBk9Hixaz7KbjimP+QNz605/XnjFcDAL2pqBg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz", + "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "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": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.7.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", @@ -3834,6 +3870,19 @@ "@tanstack/react-query": ">= 4.0.0" } }, + "node_modules/@napi-rs/wasm-runtime": { + "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": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, "node_modules/@newrelic/publish-sourcemap": { "version": "5.1.4", "resolved": "https://registry.npmjs.org/@newrelic/publish-sourcemap/-/publish-sourcemap-5.1.4.tgz", @@ -4412,6 +4461,166 @@ "@parcel/watcher-win32-x64": "2.5.1" } }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.1.tgz", + "integrity": "sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.1.tgz", + "integrity": "sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.1.tgz", + "integrity": "sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.1.tgz", + "integrity": "sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.1.tgz", + "integrity": "sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.1.tgz", + "integrity": "sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.1.tgz", + "integrity": "sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.1.tgz", + "integrity": "sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@parcel/watcher-linux-x64-glibc": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.1.tgz", @@ -4452,6 +4661,66 @@ "url": "https://opencollective.com/parcel" } }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.1.tgz", + "integrity": "sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.1.tgz", + "integrity": "sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.1.tgz", + "integrity": "sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -5009,6 +5278,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", "integrity": "sha512-ynmNeT7asXyH3aSVv4vvX4Rb+0qjOhdNHnO/3vuZNqPmhDpV/+rCSGwQ7bLcmU2cJ4dvoheIO85LQj0IbJHEtg==", "dev": true, + "license": "MIT", "dependencies": { "@adobe/css-tools": "^4.0.1", "@babel/runtime": "^7.9.2", @@ -5044,6 +5314,7 @@ "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-14.3.1.tgz", "integrity": "sha512-H99XjUhWQw0lTgyMN05W3xQG1Nh4lq574D8keFf1dDoNTJgp66VbJozRaczoF+wsiaPJNt/TcnfpLGufGxSrZQ==", "dev": true, + "license": "MIT", "dependencies": { "@babel/runtime": "^7.12.5", "@testing-library/dom": "^9.0.0", @@ -5175,6 +5446,17 @@ "node": ">=10.13.0" } }, + "node_modules/@tybys/wasm-util": { + "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": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/aria-query": { "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", @@ -5949,6 +6231,160 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.7.2.tgz", + "integrity": "sha512-vxtBno4xvowwNmO/ASL0Y45TpHqmNkAaDtz4Jqb+clmcVSSl8XCG/PNFFkGsXXXS6AMjP+ja/TtNCFFa1QwLRg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.7.2.tgz", + "integrity": "sha512-qhVa8ozu92C23Hsmv0BF4+5Dyyd5STT1FolV4whNgbY6mj3kA0qsrGPe35zNR3wAN7eFict3s4Rc2dDTPBTuFQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.7.2.tgz", + "integrity": "sha512-zKKdm2uMXqLFX6Ac7K5ElnnG5VIXbDlFWzg4WJ8CGUedJryM5A3cTgHuGMw1+P5ziV8CRhnSEgOnurTI4vpHpg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.7.2.tgz", + "integrity": "sha512-8N1z1TbPnHH+iDS/42GJ0bMPLiGK+cUqOhNbMKtWJ4oFGzqSJk/zoXFzcQkgtI63qMcUI7wW1tq2usZQSb2jxw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.7.2.tgz", + "integrity": "sha512-tjYzI9LcAXR9MYd9rO45m1s0B/6bJNuZ6jeOxo1pq1K6OBuRMMmfyvJYval3s9FPPGmrldYA3mi4gWDlWuTFGA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.7.2.tgz", + "integrity": "sha512-jon9M7DKRLGZ9VYSkFMflvNqu9hDtOCEnO2QAryFWgT6o6AXU8du56V7YqnaLKr6rAbZBWYsYpikF226v423QA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.7.2.tgz", + "integrity": "sha512-c8Cg4/h+kQ63pL43wBNaVMmOjXI/X62wQmru51qjfTvI7kmCy5uHTJvK/9LrF0G8Jdx8r34d019P1DVJmhXQpA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.7.2.tgz", + "integrity": "sha512-A+lcwRFyrjeJmv3JJvhz5NbcCkLQL6Mk16kHTNm6/aGNc4FwPHPE4DR9DwuCvCnVHvF5IAd9U4VIs/VvVir5lg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.7.2.tgz", + "integrity": "sha512-hQQ4TJQrSQW8JlPm7tRpXN8OCNP9ez7PajJNjRD1ZTHQAy685OYqPrKjfaMw/8LiHCt8AZ74rfUVHP9vn0N69Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.7.2.tgz", + "integrity": "sha512-NoAGbiqrxtY8kVooZ24i70CjLDlUFI7nDj3I9y54U94p+3kPxwd2L692YsdLa+cqQ0VoqMWoehDFp21PKRUoIQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.7.2.tgz", + "integrity": "sha512-KaZByo8xuQZbUhhreBTW+yUnOIHUsv04P8lKjQ5otiGoSJ17ISGYArc+4vKdLEpGaLbemGzr4ZeUbYQQsLWFjA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@unrs/resolver-binding-linux-x64-gnu": { "version": "1.7.2", "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.7.2.tgz", @@ -5977,6 +6413,65 @@ "linux" ] }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.7.2.tgz", + "integrity": "sha512-y797JBmO9IsvXVRCKDXOxjyAE4+CcZpla2GSoBQ33TVb3ILXuFnMrbR/QQZoauBYeOFuu4w3ifWLw52sdHGz6g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.9" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.7.2.tgz", + "integrity": "sha512-gtYTh4/VREVSLA+gHrfbWxaMO/00y+34htY7XpioBTy56YN2eBjkPrY1ML1Zys89X3RJDKVaogzwxlM1qU7egg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.7.2.tgz", + "integrity": "sha512-Ywv20XHvHTDRQs12jd3MY8X5C8KLjDbg/jyaal/QLKx3fAShhJyD4blEANInsjxW3P7isHx1Blt56iUDDJO3jg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.7.2.tgz", + "integrity": "sha512-friS8NEQfHaDbkThxopGk+LuE5v3iY0StruifjQEt7SLbA46OnfgMO15sOTkbpJkol6RB+1l1TYPXh0sCddpvA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@webassemblyjs/ast": { "version": "1.14.1", "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", @@ -8273,6 +8768,16 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/cross-fetch": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.2.0.tgz", + "integrity": "sha512-Q+xVJLoGOeIMXZmbUK4HYk+69cQH6LudR0Vu/pRm2YlU/hDV9CiS0gKUMaWY5f2NeUH9C1nV3bsTlCo0FsTV1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "node-fetch": "^2.7.0" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -11458,6 +11963,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -13812,6 +14331,17 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, + "node_modules/jest-fetch-mock": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/jest-fetch-mock/-/jest-fetch-mock-3.0.3.tgz", + "integrity": "sha512-Ux1nWprtLrdrH4XwE7O7InRY6psIi3GOsqNESJgMJ+M5cv4A8Lh7SN9d2V2kKRZ8ebAfcd1LNyZguAOb6JiDqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-fetch": "^3.0.4", + "promise-polyfill": "^8.1.3" + } + }, "node_modules/jest-get-type": { "version": "29.6.3", "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", @@ -15151,6 +15681,52 @@ "license": "MIT", "optional": true }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -17045,6 +17621,13 @@ "devOptional": true, "license": "MIT" }, + "node_modules/promise-polyfill": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-8.3.0.tgz", + "integrity": "sha512-H5oELycFml5yto/atYqmjyigJoAo3+OXwolYiH7OfQuYlAqhxNvTfiNMbV9hsC6Yp83yE5r2KTVmtrG6R9i6Pg==", + "dev": true, + "license": "MIT" + }, "node_modules/prompts": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", @@ -21657,6 +22240,13 @@ "node": ">=0.10.0" } }, + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", + "dev": true, + "license": "MIT" + }, "node_modules/whatwg-mimetype": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz", diff --git a/package.json b/package.json index 129383f9..55d61a54 100644 --- a/package.json +++ b/package.json @@ -76,12 +76,14 @@ "@openedx/frontend-build": "^14.6.0", "@testing-library/dom": "^10.4.0", "@testing-library/jest-dom": "^5.17.0", - "@testing-library/react": "^14.1.2", + "@testing-library/react": "^14.3.1", "@testing-library/user-event": "^14.6.1", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.1.4", "jest": "^29.7.0", + "jest-fetch-mock": "^3.0.3", "rosie": "^2.1.1", - "ts-jest": "^29.4.0" + "ts-jest": "^29.4.0", + "whatwg-fetch": "^3.6.20" } } diff --git a/src/components/ErrorPage/ErrorPage.tsx b/src/components/ErrorPage/ErrorPage.tsx index 1dc800db..b0355b19 100644 --- a/src/components/ErrorPage/ErrorPage.tsx +++ b/src/components/ErrorPage/ErrorPage.tsx @@ -2,7 +2,6 @@ import { getConfig } from '@edx/frontend-platform/config'; import { defineMessages, useIntl } from '@edx/frontend-platform/i18n'; import { Button, Image } from '@openedx/paragon'; import { MessageDescriptor } from 'react-intl'; -import { useRouteError } from 'react-router'; import { isRouteErrorResponse } from 'react-router-dom'; import { Footer } from '../Footer'; @@ -98,7 +97,7 @@ const getRouteErrorDerivedMessage = (routeError: UnknownError): string | undefin }; const ErrorPage = ({ message }: ErrorPageProps) => { - const routeError = useRouteError() as UnknownError; + const routeError : any = null; const derivedErrorMessage = getRouteErrorDerivedMessage(routeError); // Prefer downstream thrown error message; fall back to prop message diff --git a/src/components/PurchaseSummary/PurchaseSummary.tsx b/src/components/PurchaseSummary/PurchaseSummary.tsx index 992c0aa0..f41805db 100644 --- a/src/components/PurchaseSummary/PurchaseSummary.tsx +++ b/src/components/PurchaseSummary/PurchaseSummary.tsx @@ -1,5 +1,5 @@ import { Card, Stack } from '@openedx/paragon'; -import React from 'react'; +import React, { useEffect, useState } from 'react'; import { usePurchaseSummaryPricing } from '@/components/app/data'; import { DataStoreKey } from '@/constants/checkout'; @@ -11,32 +11,111 @@ import LicensesRow from './LicensesRow'; import PricePerUserRow from './PricePerUserRow'; import PurchaseSummaryCardButton from './PurchaseSummaryCardButton'; import PurchaseSummaryHeader from './PurchaseSummaryHeader'; +import TestimonialCard from './TestimonialCard'; import TotalAfterTrialRow from './TotalAfterTrialRow'; const PurchaseSummary: React.FC = () => { - const quantity = useCheckoutFormStore((state) => state.formData[DataStoreKey.PlanDetails]?.quantity); - const companyName = useCheckoutFormStore((state) => state.formData[DataStoreKey.AccountDetails].companyName); + const quantity = useCheckoutFormStore( + (state) => state.formData[DataStoreKey.PlanDetails]?.quantity, + ); + + const companyName = useCheckoutFormStore( + (state) => state.formData[DataStoreKey.AccountDetails].companyName, + ); + const { yearlySubscriptionCostForQuantity, yearlyCostPerSubscriptionPerUser, } = usePurchaseSummaryPricing(); - // TODO: Fix bug, quantity should be returned as a number instead of a string, we have been assuming a number const normalizedQuantity = parseInt(quantity, 10) === 0 ? null : quantity; + // ----------------------------- + // Testimonial State + // ----------------------------- + const [testimonials, setTestimonials] = useState([]); + const [currentTestimonial, setCurrentTestimonial] = useState(null); + const [shownTestimonials, setShownTestimonials] = useState([]); + + // ----------------------------- + // Fetch testimonials + // ----------------------------- + useEffect(() => { + const abortController = new AbortController(); + const url = `${process.env.ENTERPRISE_ACCESS_BASE_URL}/api/v1/testimonials/`; + + fetch(url, { signal: abortController.signal }) + .then((res) => (res.ok ? res.json() : null)) + .then((data) => { + if (!data) { return; } + + const results = data?.results || []; + setTestimonials(results); + + if (results.length > 0) { + const randomIndex = Math.floor(Math.random() * results.length); + const firstTestimonial = results[randomIndex]; + setCurrentTestimonial(firstTestimonial); + setShownTestimonials([firstTestimonial.uuid]); + } + }) + .catch((err) => { + if (err.name !== 'AbortError') { + // eslint-disable-next-line no-console + console.error('Failed to fetch testimonials:', err); + } + }); + + return () => abortController.abort(); + }, []); + + // ----------------------------- + // Rotation logic + // ----------------------------- + useEffect(() => { + if (!testimonials.length) { return; } + + let available = testimonials.filter( + (t) => !shownTestimonials.includes(t.uuid), + ); + + if (available.length === 0) { + available = testimonials; + setShownTestimonials([]); + } + + const random = available[Math.floor(Math.random() * available.length)]; + setCurrentTestimonial(random); + setShownTestimonials((prev) => [...prev, random.uuid]); + }, [quantity, testimonials, shownTestimonials]); + return ( + - + +
- - + + + + + + + {currentTestimonial && }
+ diff --git a/src/components/PurchaseSummary/TestimonialCard.tsx b/src/components/PurchaseSummary/TestimonialCard.tsx new file mode 100644 index 00000000..19d178b9 --- /dev/null +++ b/src/components/PurchaseSummary/TestimonialCard.tsx @@ -0,0 +1,29 @@ +import { Stack } from '@openedx/paragon'; +import React from 'react'; + +interface Props { + testimonial: { + quote_text: string; + attribution_name: string; + attribution_title: string; + } | null; +} + +const TestimonialCard: React.FC = ({ testimonial }) => { + if (!testimonial) { return null; } + + return ( +
+ +
+
{testimonial.quote_text}
+
+ {testimonial.attribution_name} +
{testimonial.attribution_title}
+
+
+
+ ); +}; + +export default TestimonialCard; diff --git a/src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx b/src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx index f9a572ba..f79d4aa8 100644 --- a/src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx +++ b/src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx @@ -1,5 +1,6 @@ +// src/components/PurchaseSummary/tests/PurchaseSummary.test.tsx import { IntlProvider } from '@edx/frontend-platform/i18n'; -import { render, screen } from '@testing-library/react'; +import { render, screen, waitFor } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import '@testing-library/jest-dom'; @@ -10,16 +11,51 @@ import { checkoutFormStore } from '@/hooks/useCheckoutFormStore'; import PurchaseSummary from '../PurchaseSummary'; +// ----------------------------- +// Global fetch mock for Node environment +// ----------------------------- +import 'whatwg-fetch'; + +// ----------------------------- +// Mock data hooks +// ----------------------------- jest.mock('@/components/app/data', () => ({ __esModule: true, usePurchaseSummaryPricing: jest.fn(), useCreateBillingPortalSession: jest.fn(() => ({ data: { url: null } })), useCheckoutIntent: jest.fn(() => ({ data: { id: 123 } })), -})); +})); // ensures fetch is defined in Node + +beforeAll(() => { + jest.spyOn(global, 'fetch').mockImplementation((url: string) => { + if (typeof url === 'string') { + if (url.includes('/api/v1/testimonials/')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ results: [] }), + } as Response); + } + if (url.includes('/api/v1/orders/')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ id: 123, status: 'pending' }), + } as Response); + } + } + // fallback for other URLs + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + } as Response); + }) as jest.Mock; +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); describe('PurchaseSummary', () => { beforeEach(() => { - // Seed the form store checkoutFormStore.setState((s) => ({ ...s, formData: { @@ -33,6 +69,8 @@ describe('PurchaseSummary', () => { yearlySubscriptionCostForQuantity: 150, yearlyCostPerSubscriptionPerUser: 50, }); + + (global.fetch as jest.Mock).mockClear(); }); it('renders header and rows with computed values', () => { @@ -44,28 +82,50 @@ describe('PurchaseSummary', () => { , ); - // Header - validateText('Purchase summary'); - validateText('For Acme'); + expect(screen.getByText('Purchase summary')).toBeInTheDocument(); + expect(screen.getByText('For Acme')).toBeInTheDocument(); + expect(screen.getByText('Team Subscription, price per user, paid yearly')).toBeInTheDocument(); + expect(screen.getByText('$50 USD')).toBeInTheDocument(); + expect(screen.getByText('Number of licenses')).toBeInTheDocument(); + expect(screen.getByText('x3')).toBeInTheDocument(); + expect(screen.getByText(`Total after ${SUBSCRIPTION_TRIAL_LENGTH_DAYS}-day free trial`)).toBeInTheDocument(); + expect(screen.getByText('$150 USD')).toBeInTheDocument(); + expect(screen.getByText(/Auto-renews annually/i)).toBeInTheDocument(); + expect(screen.getByText('Due today')).toBeInTheDocument(); + expect(screen.getByText('$0')).toBeInTheDocument(); + }); - // Price per user row - validateText('Team Subscription, price per user, paid yearly'); - validateText('$50 USD'); + it('calls the testimonials API on mount', async () => { + render( + + + + + , + ); - // Licenses row - validateText('Number of licenses'); - validateText('x3'); + await waitFor(() => { + expect(global.fetch).toHaveBeenCalledTimes(1); + expect(global.fetch).toHaveBeenCalledWith( + `${process.env.ENTERPRISE_ACCESS_BASE_URL}/api/v1/testimonials/`, + expect.any(Object), + ); + }); + }); - // Total after trial row - validateText(`Total after ${SUBSCRIPTION_TRIAL_LENGTH_DAYS}-day free trial`); - validateText('$150 USD'); - expect(screen.getAllByText(/\/yr/).length).toBeGreaterThan(0); + it('handles fetch failure gracefully', async () => { + (global.fetch as jest.Mock).mockImplementationOnce(() => Promise.reject(new Error('API failure'))); - // Auto renew notice - validateText(/Auto-renews annually/i); + render( + + + + + , + ); - // Due today row - validateText('Due today'); - validateText('$0'); + await waitFor(() => { + expect(screen.getByText('Purchase summary')).toBeInTheDocument(); + }); }); }); diff --git a/src/setupTest.ts b/src/setupTest.ts index 4a2d38f9..a5e56d22 100644 --- a/src/setupTest.ts +++ b/src/setupTest.ts @@ -1,92 +1,81 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import { screen } from '@testing-library/react'; +// src/setupTest.ts -// @ts-ignore -import type { TextMatch } from '@testing-library/dom'; - -// Suppress specific console.error warnings /* eslint-disable no-console */ +import { screen, waitFor } from '@testing-library/react'; -// TODO: Once there are no more console errors in tests, uncomment the code below -// const { error } = global.console; - -// global.console.error = (...args) => { -// error(...args); -// throw new Error(args.join(' ')); -// }; +import type { TextMatch } from '@testing-library/dom'; +import 'whatwg-fetch'; +// ------------------ Console suppression ------------------ const originalConsoleError = console.error; const originalConsoleWarn = console.warn; const CONSOLE_FILTERS = { - warn: [ - 'PubSub already loaded', - ], - error: [ - 'Support for defaultProps will be removed from function components', - ], + warn: ['PubSub already loaded'], + error: ['Support for defaultProps will be removed from function components'], }; -// Override `console.error` -console.error = (...args) => { +console.error = (...args: any[]) => { const message = args[0]; - if ( - typeof message === 'string' - && CONSOLE_FILTERS.error.some(ignored => message.includes(ignored)) - ) { + if (typeof message === 'string' && CONSOLE_FILTERS.error.some(ignored => message.includes(ignored))) { return; } originalConsoleError(...args); }; -// Override `console.warn` -console.warn = (...args) => { +console.warn = (...args: any[]) => { const message = args[0]; - if ( - typeof message === 'string' - && CONSOLE_FILTERS.warn.some(ignored => message.includes(ignored)) - ) { + if (typeof message === 'string' && CONSOLE_FILTERS.warn.some(ignored => message.includes(ignored))) { return; } originalConsoleWarn(...args); }; /* eslint-enable no-console */ -// @ts-ignore -// eslint-disable-next-line func-names -global.validateText = function ( - text: TextMatch, - options?: { - exact?: boolean; - selector?: string; - ignore?: string | boolean; - normalizer?: (text: string) => string; - }, +// ------------------ Global fetch polyfill ------------------ +beforeAll(() => { + global.fetch = global.fetch || (jest.fn((url: string) => { + if (url.includes('/admin/check-email')) { + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({ registered: false }), + }); + } + return Promise.resolve({ + ok: true, + json: () => Promise.resolve({}), + }); + }) as jest.Mock); +}); + +afterAll(() => { + jest.restoreAllMocks(); +}); + +// ------------------ Global validateText helper ------------------ +global.validateText = async function ( + text: string | RegExp | ((content: string) => boolean), + options?: { exact?: boolean }, + timeout = 5000, // increased timeout for slower async pages ) { - expect(screen.getByText(text, options)).toBeInTheDocument(); + let element; + + await waitFor(() => { + if (typeof text === 'string' || text instanceof RegExp) { + element = screen.queryByText(text as TextMatch, options); + } else if (typeof text === 'function') { + element = Array.from(document.body.querySelectorAll('*')).find(el => el.textContent && text(el.textContent)); + } + + if (!element) { + throw new Error(`Unable to find element with text: ${text}`); + } + + expect(element).toBeInTheDocument(); + }, { timeout }); }; -// Global helper to validate debounced behavior in a deterministic, reusable way -// Usage example: -// await assertDebounce({ -// baseDelayMs: 500, -// preCalls: [() => debouncedFn(arg1)], -// call: () => debouncedFn(arg2), -// getInvocationCount: () => mockFn.mock.calls.length, -// }); -// It will: -// - Use modern fake timers -// - Assert nothing runs before baseDelayMs -// - Advance time in phases and verify only one invocation occurs -// - Measure elapsed time from the last call until the promise resolves -// - Restore real timers at the end -// Notes: -// - preCalls are invoked synchronously before measuring the final call -// - call must return a Promise that settles when the debounced work completes -// - getInvocationCount is optional; if provided, assertions on invocations are made -// - You can override the upper margin via upperMarginMs (default 20ms) -// - This helper sets and restores jest timers; avoid toggling timers inside call/preCalls -// eslint-disable-next-line @typescript-eslint/no-explicit-any +// ------------------ Global debounce helper ------------------ (global as any).assertDebounce = async function assertDebounce(options: { baseDelayMs: number; call: () => Promise; @@ -94,31 +83,17 @@ global.validateText = function ( getInvocationCount?: () => number; upperMarginMs?: number; }) { - const { - baseDelayMs, - call, - preCalls = [], - getInvocationCount, - upperMarginMs = 20, - } = options; - - // Use modern fake timers to drive debounce deterministically + const { baseDelayMs, call, preCalls = [], getInvocationCount, upperMarginMs = 20 } = options; + jest.useFakeTimers({ legacyFakeTimers: false }); - // Execute any preliminary rapid calls to be debounced for (const fn of preCalls) { - try { - fn(); - } catch (e) { - // ignore sync errors from preCalls; actual validation will happen on final call - } + try { fn(); } catch { /* ignore sync errors */ } } - // Start measuring from the LAST call const start = Date.now(); const p = call(); - // Advance time to just before the debounce threshold in two steps const before = Math.max(0, baseDelayMs - 100); if (before > 0) { jest.advanceTimersByTime(before); @@ -131,28 +106,21 @@ global.validateText = function ( await Promise.resolve(); } - // Verify nothing has executed yet if we can observe invocations if (getInvocationCount) { expect(getInvocationCount()).toBe(0); } - // Cross the boundary by 1ms to trigger the debounced execution jest.advanceTimersByTime(1); - - // Await completion of the debounced promise await p; - // Optionally assert exactly one invocation happened if (getInvocationCount) { expect(getInvocationCount()).toBe(1); } - // Measure elapsed and assert timing window const elapsed = Date.now() - start; expect(elapsed).toBeGreaterThanOrEqual(baseDelayMs); expect(elapsed).toBeLessThanOrEqual(baseDelayMs + upperMarginMs); - // Always restore real timers jest.useRealTimers(); return { elapsedMs: elapsed }; diff --git a/tsconfig.json b/tsconfig.json index 94a5a7f6..a3789f9e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -3,6 +3,8 @@ "compilerOptions": { "rootDir": ".", "outDir": "dist", + "jsx": "react-jsx", + "esModuleInterop": true, "paths": { "@/components/*": ["./src/components/*"], "@/hooks/*": ["./src/hooks/*"], diff --git a/webpack.dev-stage.config.js b/webpack.dev-stage.config.js index d6464bbd..0095ebff 100644 --- a/webpack.dev-stage.config.js +++ b/webpack.dev-stage.config.js @@ -22,9 +22,33 @@ const config = createConfig('webpack-dev', { devServer: { allowedHosts: 'all', server: 'https', + port: 2012, + proxy: { + '/api/catalog/**': { + target: 'http://localhost:18160', // Enterprise Catalog API + changeOrigin: true, + secure: false, + }, + '/api/access/**': { + target: 'http://localhost:18270', // Enterprise Access API + changeOrigin: true, + secure: false, + }, + '/api/ecommerce/**': { + target: 'http://localhost:18130', // Ecommerce API + changeOrigin: true, + secure: false, + }, + '/csrf/api/v1/token': { + target: 'http://localhost:18270', // CSRF token endpoint (example) + changeOrigin: true, + secure: false, + }, + }, }, }); +// Fix module rule for edX packages config.module.rules[0].exclude = /node_modules\/(?!(lodash-es|@(open)?edx))/; module.exports = config;