diff --git a/.gitignore b/.gitignore index aea1036b67..2f734f5f94 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,7 @@ node_modules dist dist-ssr *.local +coverage # Editor directories and files .vscode/* diff --git a/global.css b/global.css deleted file mode 100644 index 3d810a1bd9..0000000000 --- a/global.css +++ /dev/null @@ -1,15 +0,0 @@ -.material-symbols-rounded { - font-family: 'MaterialSymbolsRounded'; - font-weight: normal; - font-style: normal; - font-size: 24px; - line-height: 1; - letter-spacing: normal; - text-transform: none; - display: inline-block; - white-space: nowrap; - word-wrap: normal; - direction: ltr; - -webkit-font-feature-settings: 'liga'; - -webkit-font-smoothing: antialiased; -} diff --git a/index.html b/index.html deleted file mode 100644 index e4b78eae12..0000000000 --- a/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - Vite + React + TS - - -
- - - diff --git a/package-lock.json b/package-lock.json index c1aade4607..ddda8d8a62 100644 --- a/package-lock.json +++ b/package-lock.json @@ -57,6 +57,7 @@ "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "@vitejs/plugin-react": "^4.3.1", + "@vitest/coverage-v8": "^3.2.6", "colors": "^1.4.0", "commander": "^14.0.0", "commitizen": "^4.3.1", @@ -133,6 +134,20 @@ "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", "dev": true }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/@asamuzakjp/css-color": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", @@ -432,6 +447,16 @@ "node": ">=6.9.0" } }, + "node_modules/@bcoe/v8-coverage": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-1.0.2.tgz", + "integrity": "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@colors/colors": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", @@ -1428,6 +1453,119 @@ "deprecated": "Use @eslint/object-schema instead", "dev": true }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", + "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.2.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.6.tgz", + "integrity": "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.13", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", @@ -3124,6 +3262,17 @@ "@octokit/openapi-types": "^27.0.0" } }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, "node_modules/@pnpm/config.env-replace": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", @@ -4528,6 +4677,7 @@ "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", "dev": true, + "license": "MIT", "dependencies": { "@types/deep-eql": "*", "assertion-error": "^2.0.1" @@ -4547,7 +4697,8 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/@types/estree": { "version": "1.0.8", @@ -4896,15 +5047,50 @@ "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, + "node_modules/@vitest/coverage-v8": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-3.2.6.tgz", + "integrity": "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@bcoe/v8-coverage": "^1.0.2", + "ast-v8-to-istanbul": "^0.3.3", + "debug": "^4.4.1", + "istanbul-lib-coverage": "^3.2.2", + "istanbul-lib-report": "^3.0.1", + "istanbul-lib-source-maps": "^5.0.6", + "istanbul-reports": "^3.1.7", + "magic-string": "^0.30.17", + "magicast": "^0.3.5", + "std-env": "^3.9.0", + "test-exclude": "^7.0.1", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@vitest/browser": "3.2.6", + "vitest": "3.2.6" + }, + "peerDependenciesMeta": { + "@vitest/browser": { + "optional": true + } + } + }, "node_modules/@vitest/expect": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", - "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" }, @@ -4913,12 +5099,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", - "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/spy": "3.2.4", + "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, @@ -4943,15 +5130,17 @@ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", "dev": true, + "license": "MIT", "dependencies": { "@types/estree": "^1.0.0" } }, "node_modules/@vitest/pretty-format": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", - "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", "dev": true, + "license": "MIT", "dependencies": { "tinyrainbow": "^2.0.0" }, @@ -4960,12 +5149,13 @@ } }, "node_modules/@vitest/runner": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", - "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/utils": "3.2.4", + "@vitest/utils": "3.2.6", "pathe": "^2.0.3", "strip-literal": "^3.0.0" }, @@ -4974,12 +5164,13 @@ } }, "node_modules/@vitest/snapshot": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", - "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.6", "magic-string": "^0.30.17", "pathe": "^2.0.3" }, @@ -4988,10 +5179,11 @@ } }, "node_modules/@vitest/spy": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", - "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", "dev": true, + "license": "MIT", "dependencies": { "tinyspy": "^4.0.3" }, @@ -5000,12 +5192,13 @@ } }, "node_modules/@vitest/utils": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", - "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", "dev": true, + "license": "MIT", "dependencies": { - "@vitest/pretty-format": "3.2.4", + "@vitest/pretty-format": "3.2.6", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" }, @@ -5444,10 +5637,40 @@ "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" } }, + "node_modules/ast-v8-to-istanbul": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", + "integrity": "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.31", + "estree-walker": "^3.0.3", + "js-tokens": "^10.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/ast-v8-to-istanbul/node_modules/js-tokens": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-10.0.0.tgz", + "integrity": "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/astral-regex": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-1.0.0.tgz", @@ -5772,6 +5995,7 @@ "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", "dev": true, + "license": "MIT", "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", @@ -5826,6 +6050,7 @@ "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", "dev": true, + "license": "MIT", "engines": { "node": ">= 16" } @@ -6711,6 +6936,7 @@ "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=6" } @@ -6970,6 +7196,13 @@ "safe-buffer": "~5.1.0" } }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, "node_modules/electron-to-chromium": { "version": "1.5.267", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", @@ -8082,6 +8315,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/form-data": { "version": "4.0.5", "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", @@ -8397,6 +8647,28 @@ "traverse": "0.6.8" } }, + "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", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -8409,6 +8681,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/global-directory": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", @@ -8731,6 +9029,13 @@ "node": ">=18" } }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, "node_modules/http-proxy-agent": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", @@ -9569,6 +9874,83 @@ "node": "^18.17 || >=20.6.1" } }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "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==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/iterator.prototype": { "version": "1.1.5", "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", @@ -9586,6 +9968,22 @@ "node": ">= 0.4" } }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, "node_modules/java-properties": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", @@ -10380,7 +10778,8 @@ "version": "3.2.1", "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/lower-case": { "version": "2.0.2", @@ -10417,6 +10816,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/magicast": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.3.5.tgz", + "integrity": "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.25.4", + "@babel/types": "^7.25.4", + "source-map-js": "^1.2.0" + } + }, "node_modules/make-asynchronous": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.0.1.tgz", @@ -10446,6 +10857,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/markdown-eslint-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/markdown-eslint-parser/-/markdown-eslint-parser-1.2.1.tgz", @@ -11183,6 +11623,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -13227,7 +13677,7 @@ } }, "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { - "version": "4.0.4", + "version": "4.0.3", "dev": true, "inBundle": true, "license": "MIT", @@ -13721,6 +14171,13 @@ "node": ">=4" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -13834,6 +14291,30 @@ "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, "node_modules/path-type": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", @@ -13852,6 +14333,7 @@ "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.16" } @@ -15652,6 +16134,32 @@ "node": ">=8" } }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/string-width/node_modules/is-fullwidth-code-point": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", @@ -15766,6 +16274,20 @@ "node": ">=8" } }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-bom": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", @@ -15815,6 +16337,7 @@ "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", "dev": true, + "license": "MIT", "dependencies": { "js-tokens": "^9.0.1" }, @@ -15826,7 +16349,8 @@ "version": "9.0.1", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/stylis": { "version": "4.2.0", @@ -16079,6 +16603,21 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/test-exclude": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-7.0.2.tgz", + "integrity": "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^10.4.1", + "minimatch": "^10.2.2" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", @@ -16241,6 +16780,7 @@ "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=14.0.0" } @@ -16842,19 +17382,20 @@ } }, "node_modules/vitest": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", - "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", "dev": true, + "license": "MIT", "dependencies": { "@types/chai": "^5.2.2", - "@vitest/expect": "3.2.4", - "@vitest/mocker": "3.2.4", - "@vitest/pretty-format": "^3.2.4", - "@vitest/runner": "3.2.4", - "@vitest/snapshot": "3.2.4", - "@vitest/spy": "3.2.4", - "@vitest/utils": "3.2.4", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", @@ -16884,8 +17425,8 @@ "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", - "@vitest/browser": "3.2.4", - "@vitest/ui": "3.2.4", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", "happy-dom": "*", "jsdom": "*" }, @@ -17149,6 +17690,61 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, "node_modules/wrap-ansi/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", diff --git a/package.json b/package.json index 1f786bf421..93bb647a5f 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,7 @@ "preview": "vite preview", "prepare": "husky", "test": "vitest run", + "test:coverage": "vitest run --coverage", "watch": "vitest", "i18n": "node scripts/i18n.js", "logBuildDate": "echo 'Last build: '$(date \"+%c\") | tee ./dist/lastBuild.txt", @@ -89,6 +90,7 @@ "@typescript-eslint/eslint-plugin": "^8.56.1", "@typescript-eslint/parser": "^8.56.1", "@vitejs/plugin-react": "^4.3.1", + "@vitest/coverage-v8": "^3.2.6", "colors": "^1.4.0", "commander": "^14.0.0", "commitizen": "^4.3.1", diff --git a/src/Connect-test.tsx b/src/Connect-test.tsx new file mode 100644 index 0000000000..bb736254e2 --- /dev/null +++ b/src/Connect-test.tsx @@ -0,0 +1,264 @@ +import React from 'react' +import { beforeEach, describe, it, expect, vi, afterEach } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { Connect } from './Connect' +import { initialState, masterData } from 'src/services/mockedData' +import { STEPS } from 'src/const/Connect' + +describe('', () => { + const mockPostMessage = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + Object.defineProperty(window, 'parent', { + writable: true, + configurable: true, + value: { + postMessage: mockPostMessage, + }, + }) + Object.defineProperty(window, 'top', { + writable: true, + configurable: true, + value: {}, + }) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + const defaultProps: ConnectProps = { + clientConfig: {} as ClientConfigType, + profiles: { loading: false, ...masterData }, + userFeatures: {}, + experimentalFeatures: {}, + availableAccountTypes: [] as [], + onManualAccountAdded: vi.fn(), + onMemberDeleted: vi.fn(), + onSuccessfulAggregation: vi.fn(), + onUpsertMember: vi.fn(), + onAnalyticEvent: vi.fn(), + onAnalyticPageview: vi.fn(), + onShowConnectSuccessSurvey: () => {}, + onSubmitConnectSuccessSurvey: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('loading states', () => { + it('displays loading spinner when component is loading', () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: true, + }, + } + + render(, { preloadedState }) + + expect(screen.getByText(/Loading/i)).toBeInTheDocument() + }) + + it('renders without crashing when there is a config error', () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + loadError: { + type: 'config', + title: 'Configuration Error', + message: 'This mode is not available for your account', + }, + }, + } + + const { container } = render(, { preloadedState }) + + expect(container).toBeInTheDocument() + }) + + it('renders without crashing when there is a network error', () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + loadError: { + type: 'network', + title: 'Network Error', + message: 'Unable to connect to the server', + }, + }, + } + + const { container } = render(, { preloadedState }) + + expect(container).toBeInTheDocument() + }) + }) + + describe('legacy Atrium API support', () => { + it('sends legacy post message for Atrium with old ui_message_version', async () => { + const preloadedState = { + ...initialState, + profiles: { + ...initialState.profiles, + client: { + ...initialState.profiles.client, + has_atrium_api: true, + }, + }, + config: { + ...initialState.config, + is_mobile_webview: false, + ui_message_version: 3, + }, + connect: { + ...initialState.connect, + isComponentLoading: false, + }, + } + + render(, { preloadedState }) + + await waitFor(() => { + expect(mockPostMessage).toHaveBeenCalled() + const callArgs = mockPostMessage.mock.calls[0] + const messageData = JSON.parse(callArgs[0]) + expect(messageData.type).toBe('mxConnect:widgetLoaded') + }) + }) + }) + + describe('version metadata', () => { + it('stores version prop in redux state', async () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + }, + } + + const { store } = render(, { preloadedState }) + + await waitFor(() => { + expect(store.getState().app.version).toBe('v1.2.3') + }) + }) + + it('handles missing version prop', async () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + }, + } + + const { store } = render(, { preloadedState }) + + await waitFor(() => { + const version = store.getState().app.version + expect(version === null || version === undefined).toBe(true) + }) + }) + }) + + describe('profiles loading', () => { + it('loads profiles on mount', async () => { + const customProfiles = { + loading: false, + ...masterData, + client: { ...masterData.client, name: 'Custom Client Name' }, + } + + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + }, + } + + const { store } = render(, { + preloadedState, + }) + + await waitFor(() => { + expect(store.getState().profiles.client.name).toBe('Custom Client Name') + }) + }) + }) + + describe('renders main connect flow', () => { + it('renders search view when on search step', async () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + location: [{ step: STEPS.SEARCH }], + }, + } + + render(, { preloadedState }) + + await waitFor(() => { + expect(screen.getByTestId('search-input')).toBeInTheDocument() + }) + }) + + it('includes ConnectNavigationHeader in the rendered output', async () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + location: [{ step: STEPS.SEARCH }], + }, + } + + render(, { preloadedState }) + + await waitFor(() => { + expect(document.querySelector('#connect-wrapper')).toBeInTheDocument() + }) + }) + }) + + describe('analytic context provider', () => { + it('provides analytic callbacks to child components', async () => { + const onAnalyticEvent = vi.fn() + const onAnalyticPageview = vi.fn() + const onSubmitConnectSuccessSurvey = vi.fn() + + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + isComponentLoading: false, + location: [{ step: STEPS.SEARCH }], + }, + } + render( + , + { preloadedState }, + ) + + await waitFor(() => { + expect(screen.getByTestId('search-input')).toBeInTheDocument() + }) + }) + }) +}) diff --git a/src/Connect.tsx b/src/Connect.tsx index f44c31faad..267e07bff1 100644 --- a/src/Connect.tsx +++ b/src/Connect.tsx @@ -328,25 +328,19 @@ export const Connect: React.FC = ({ return (
- {state.memberToDelete && ( - { - setState({ ...state, memberToDelete: null }) - }} - onDeleteSuccess={(deletedMember) => { - postMessageFunctions.onPostMessage('connect/memberDeleted', { - member_guid: deletedMember.guid, - }) - onMemberDeleted(deletedMember.guid) - - setState((prevState) => { - dispatch(connectActions.stepToDeleteMemberSuccess(deletedMember.guid)) - return { ...prevState, memberToDelete: null } - }) - }} - /> - )} + setState({ ...state, memberToDelete: null })} + onMemberDeleted={(memberGuid) => { + postMessageFunctions.onPostMessage('connect/memberDeleted', { + member_guid: memberGuid, + }) + onMemberDeleted(memberGuid) + dispatch(connectActions.stepToDeleteMemberSuccess(memberGuid)) + setState({ ...state, memberToDelete: null }) + }} + /> dispatch(handleGoBackWithSideEffects())} diff --git a/src/components/ConnectInstitutionHeader-test.tsx b/src/components/ConnectInstitutionHeader-test.tsx new file mode 100644 index 0000000000..47e875a3f8 --- /dev/null +++ b/src/components/ConnectInstitutionHeader-test.tsx @@ -0,0 +1,56 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { ConnectInstitutionHeader } from 'src/components/ConnectInstitutionHeader' +import { COLOR_SCHEME } from 'src/const/Connect' +import { initialState } from 'src/services/mockedData' + +describe('ConnectInstitutionHeader', () => { + const createPreloadedState = (colorScheme: string) => ({ + ...initialState, + config: { + ...initialState.config, + color_scheme: colorScheme, + }, + }) + + it('renders light backdrop when color scheme is LIGHT', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) + + expect(screen.getByTestId('backdrop-light')).toBeInTheDocument() + expect(screen.queryByTestId('backdrop-dark')).not.toBeInTheDocument() + }) + + it('renders dark backdrop when color scheme is DARK', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + render(, { preloadedState }) + + expect(screen.getByTestId('backdrop-dark')).toBeInTheDocument() + expect(screen.queryByTestId('backdrop-light')).not.toBeInTheDocument() + }) + + it('renders HeaderDevice', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) + + expect(screen.getByTestId('device-svg')).toBeInTheDocument() + }) + + it('renders InstitutionLogo when institutionGuid is provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { + preloadedState, + }) + + expect(screen.getByTestId('institution-logo')).toBeInTheDocument() + expect(screen.queryByTestId('default-institution-icon')).not.toBeInTheDocument() + }) + + it('renders default institution icon when institutionGuid is not provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + render(, { preloadedState }) + + expect(screen.getByTestId('default-institution-icon')).toBeInTheDocument() + }) +}) diff --git a/src/components/ConnectInstitutionHeader.js b/src/components/ConnectInstitutionHeader.js index 6539ad5098..b2985b40a5 100644 --- a/src/components/ConnectInstitutionHeader.js +++ b/src/components/ConnectInstitutionHeader.js @@ -22,16 +22,25 @@ export const ConnectInstitutionHeader = (props) => { return (
-
- {colorScheme === COLOR_SCHEME.LIGHT ? : } +
+ {colorScheme === COLOR_SCHEME.LIGHT ? ( + + ) : ( + + )}
- +
{props.institutionGuid ? ( - + ) : ( - + )}
diff --git a/src/components/Container-test.tsx b/src/components/Container-test.tsx new file mode 100644 index 0000000000..7e9821df16 --- /dev/null +++ b/src/components/Container-test.tsx @@ -0,0 +1,35 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render } from 'src/utilities/testingLibrary' +import { Container } from 'src/components/Container' +import { STEPS } from 'src/const/Connect' +import { initialState } from 'src/services/mockedData' + +describe('Container', () => { + const preloadedState = initialState + + it('renders', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) + }) + + it('applies maxHeight when step is SEARCH', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) + }) +}) diff --git a/src/components/Container.js b/src/components/Container.js deleted file mode 100644 index 51d48c7c1c..0000000000 --- a/src/components/Container.js +++ /dev/null @@ -1,40 +0,0 @@ -import React from 'react' -import PropTypes from 'prop-types' - -import { useTokens } from '@kyper/tokenprovider' - -import { STEPS } from 'src/const/Connect' -/** - * Our root container to handle our widgets min/max widths, positioning and padding for all views - */ -export const Container = (props) => { - const tokens = useTokens() - const styles = getStyles(tokens, props.step) - - return ( -
-
{props.children}
-
- ) -} -Container.propTypes = { - step: PropTypes.string, -} - -const getStyles = (tokens, step) => { - return { - container: { - backgroundColor: tokens.BackgroundColor.Container, - minHeight: '100%', - maxHeight: step === STEPS.SEARCH ? '100%' : null, - display: 'flex', - justifyContent: 'center', - }, - content: { - maxWidth: '400px', // Our max content width (does not include side margin) - minWidth: '270px', // Our min content width (does not include side margin) - width: '100%', // We want this container to shrink and grow between our min-max - margin: tokens.Spacing.Large, - }, - } -} diff --git a/src/components/Container.tsx b/src/components/Container.tsx index 7fa7c9f0ae..b44d8766a7 100644 --- a/src/components/Container.tsx +++ b/src/components/Container.tsx @@ -1,9 +1,11 @@ import React from 'react' import { useTokens } from '@kyper/tokenprovider' +import { STEPS } from 'src/const/Connect' interface ContainerProps { children?: React.ReactNode + step?: string } /** @@ -11,7 +13,7 @@ interface ContainerProps { */ export const Container: React.FC = (props) => { const tokens = useTokens() - const styles = getStyles(tokens) + const styles = getStyles(tokens, props.step) return (
@@ -21,11 +23,12 @@ export const Container: React.FC = (props) => { } // eslint-disable-next-line @typescript-eslint/no-explicit-any -const getStyles = (tokens: any) => { +const getStyles = (tokens: any, step?: string) => { return { container: { backgroundColor: tokens.BackgroundColor.Container, minHeight: '100%', + maxHeight: step === STEPS.SEARCH ? '100%' : undefined, display: 'flex', justifyContent: 'center', }, diff --git a/src/components/DeleteMemberSurvey-test.tsx b/src/components/DeleteMemberSurvey-test.tsx new file mode 100644 index 0000000000..8126183264 --- /dev/null +++ b/src/components/DeleteMemberSurvey-test.tsx @@ -0,0 +1,250 @@ +import React from 'react' +import { describe, it, expect, vi } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { DeleteMemberSurvey, DELETE_REASONS } from 'src/components/DeleteMemberSurvey' +import { initialState, CONNECTED_MEMBER } from 'src/services/mockedData' +import userEvent from '@testing-library/user-event' +import { apiValue as mockApiValue } from 'src/const/apiProviderMock' +import { ReadableStatuses } from 'src/const/Statuses' + +describe('DeleteMemberSurvey', () => { + const preloadedState = initialState + + it('does not render when isOpen is false', () => { + const { container } = render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(container.firstChild).toBeNull() + }) + + it('renders when isOpen is true', () => { + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Chase Bank') + }) + + it('calls onClose when cancel button clicked', async () => { + const user = userEvent.setup() + const onClose = vi.fn() + render( + {}} + />, + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-cancel-button')) + + expect(onClose).toHaveBeenCalledTimes(1) + }) + + it('shows connected member reasons', () => { + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText(DELETE_REASONS.NO_LONGER_USE_ACCOUNT)).toBeInTheDocument() + expect(screen.getByText(DELETE_REASONS.DONT_WANT_SHARE_DATA)).toBeInTheDocument() + expect(screen.queryByText(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT)).not.toBeInTheDocument() + }) + + it('shows non-connected member reasons', () => { + const nonConnectedMember = { + ...CONNECTED_MEMBER, + connection_status: ReadableStatuses.PREVENTED, + } + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT)).toBeInTheDocument() + expect(screen.getByText(DELETE_REASONS.ACCOUNT_INFORMATION_OLD)).toBeInTheDocument() + expect(screen.queryByText(DELETE_REASONS.NO_LONGER_USE_ACCOUNT)).not.toBeInTheDocument() + }) + + it('shows validation error when no reason selected', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + }) + + it('allows selecting a reason', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + + expect(firstReason).toBeChecked() + }) + + it('clears validation error after selecting a reason', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + + it('successfully deletes member when reason selected', async () => { + const user = userEvent.setup() + const deleteMemberSpy = vi.fn(() => Promise.resolve()) + const onClose = vi.fn() + const onMemberDeleted = vi.fn() + const apiValue = { + ...mockApiValue, + deleteMember: deleteMemberSpy, + } + + render( + , + { apiValue, preloadedState }, + ) + + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(deleteMemberSpy).toHaveBeenCalledWith(CONNECTED_MEMBER) + }) + + await waitFor(() => { + expect(onMemberDeleted).toHaveBeenCalledWith(CONNECTED_MEMBER.guid) + expect(onClose).toHaveBeenCalled() + }) + }) + + it('shows error message when delete fails', async () => { + const user = userEvent.setup() + const apiValue = { + ...mockApiValue, + deleteMember: vi.fn(() => Promise.reject(new Error('Delete failed'))), + } + + render( + {}} + onMemberDeleted={() => {}} + />, + { apiValue, preloadedState }, + ) + + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument() + }) + + expect(screen.getByText('Something went wrong')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-error-message')).toBeInTheDocument() + }) + + it('dismisses error dialog when ok clicked', async () => { + const user = userEvent.setup() + const onClose = vi.fn() + const apiValue = { + ...mockApiValue, + deleteMember: vi.fn(() => Promise.reject(new Error('Delete failed'))), + } + + render( + {}} + />, + { apiValue, preloadedState }, + ) + + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument() + }) + + await user.click(screen.getByTestId('disconnect-ok-button')) + + expect(onClose).toHaveBeenCalled() + }) +}) diff --git a/src/components/DeleteMemberSurvey.js b/src/components/DeleteMemberSurvey.js index 9d8565541f..6b76b5587f 100644 --- a/src/components/DeleteMemberSurvey.js +++ b/src/components/DeleteMemberSurvey.js @@ -18,8 +18,18 @@ import useAnalyticsPath from 'src/hooks/useAnalyticsPath' import { PageviewInfo } from 'src/const/Analytics' import { ReadableStatuses } from 'src/const/Statuses' +export const DELETE_REASONS = { + NO_LONGER_USE_ACCOUNT: "I no longer use this account or it's not mine", + DONT_WANT_SHARE_DATA: "I don't want to share my data", + ACCOUNT_INFORMATION_OLD: 'The account information is old or inaccurate', + UNABLE_CONNECT_ACCOUNT: 'I am unable to connect this account here', + DONT_WANT_TO_USE_APP: "I don't want to use this app", + DONT_WANT_ACCOUNT_CONNECTED: "I don't want this account connected here", + OTHER_REASON: 'Other', +} + export const DeleteMemberSurvey = (props) => { - const { member, onCancel, onDeleteSuccess } = props + const { isOpen, member, onClose, onMemberDeleted } = props const containerRef = useRef(null) useAnalyticsPath(...PageviewInfo.CONNECT_DELETE_MEMBER_SURVEY) const { api } = useApi() @@ -32,39 +42,34 @@ export const DeleteMemberSurvey = (props) => { const tokens = useTokens() const styles = getStyles(tokens) - const DELETE_REASONS = { - NO_LONGER_USE_ACCOUNT: __("I no longer use this account or it's not mine"), - DONT_WANT_SHARE_DATA: __("I don't want to share my data"), - ACCOUNT_INFORMATION_OLD: __('The account information is old or inaccurate'), - UNABLE_CONNECT_ACCOUNT: __('I am unable to connect this account here'), - DONT_WANT_TO_USE_APP: __("I don't want to use this app"), - DONT_WANT_ACCOUNT_CONNECTED: __("I don't want this account connected here"), - OTHER_REASON: __('Other'), - } - const CONNECTED_REASONS = [ - DELETE_REASONS.NO_LONGER_USE_ACCOUNT, - DELETE_REASONS.DONT_WANT_SHARE_DATA, - DELETE_REASONS.DONT_WANT_TO_USE_APP, - DELETE_REASONS.OTHER_REASON, + __(DELETE_REASONS.NO_LONGER_USE_ACCOUNT), + __(DELETE_REASONS.DONT_WANT_SHARE_DATA), + __(DELETE_REASONS.DONT_WANT_TO_USE_APP), + __(DELETE_REASONS.OTHER_REASON), ] const NON_CONECTED_REASONS = [ - DELETE_REASONS.UNABLE_CONNECT_ACCOUNT, - DELETE_REASONS.ACCOUNT_INFORMATION_OLD, - DELETE_REASONS.DONT_WANT_ACCOUNT_CONNECTED, - DELETE_REASONS.OTHER_REASON, + __(DELETE_REASONS.UNABLE_CONNECT_ACCOUNT), + __(DELETE_REASONS.ACCOUNT_INFORMATION_OLD), + __(DELETE_REASONS.DONT_WANT_ACCOUNT_CONNECTED), + __(DELETE_REASONS.OTHER_REASON), ] useEffect(() => { if (deleteMemberState.loading === false) return () => {} const request$ = defer(() => api.deleteMember(member)).subscribe( - () => onDeleteSuccess(member), + () => { + onMemberDeleted(member.guid) + onClose() + }, (err) => updateDeleteMemberState({ loading: false, error: err }), ) return () => request$.unsubscribe() - }, [deleteMemberState.loading]) + }, [deleteMemberState.loading, api, member, onMemberDeleted, onClose]) + + if (!isOpen || !member) return null let reasonList @@ -109,7 +114,7 @@ export const DeleteMemberSurvey = (props) => {
-
- - - {__('This browser is not supported')} - - - { - // --TR: Full String: "We no longer support Internet Explorer. You can continue, or switch to a supported browser, like Edge, Chrome, or Firefox, for a better experience." - __( - 'We no longer support Internet Explorer. You can continue, or switch to a supported browser, like ', - ) - } - - {__('Edge')} - - {', '} - - {__('Chrome')} - - {', or '} - - {__('Firefox')} - - {', '} - {__(' for a better experience.')} - - - - {__( - 'Clicking the links to supported browsers will take you to an external website with a different privacy policy, security measures, and terms and conditions.', - )} - -
- ) : null -} - -const getStyles = (tokens) => ({ - container: { - background: tokens.BackgroundColor.Modal, - display: 'flex', - flexDirection: 'column', - alignItems: 'center', - padding: `0 ${tokens.Spacing.ContainerSidePadding}px`, - position: 'fixed', - top: 0, - left: 0, - right: 0, - bottom: 0, - maxWidth: '352px', // Our max content width (does not include side margin) - minWidth: '270px', // Our min content width (does not include side margin) - margin: '0 auto', - }, - header: { - position: 'absolute', - display: 'flex', - justifyContent: 'flex-end', - width: '100%', - }, - closeButton: { - marginTop: tokens.Spacing.XSmall, - }, - title: { - textAlign: 'center', - marginBottom: tokens.Spacing.Tiny, - }, - paragraph: { - textAlign: 'center', - }, - continueButton: { - marginTop: tokens.Spacing.XLarge, - marginBottom: tokens.Spacing.Medium, - }, - icon: { - marginBottom: tokens.Spacing.Large, - marginTop: tokens.Spacing.Jumbo, - paddingTop: tokens.Spacing.Tiny, - }, -}) - -IEDeprecationDialog.propTypes = { - onAnalyticPageview: PropTypes.func.isRequired, -} diff --git a/src/context/ApiContext-test.tsx b/src/context/ApiContext-test.tsx new file mode 100644 index 0000000000..06a4dba027 --- /dev/null +++ b/src/context/ApiContext-test.tsx @@ -0,0 +1,104 @@ +import React from 'react' +import { describe, it, expect, vi } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { initialState } from 'src/services/mockedData' +import { ApiProvider, useApi } from 'src/context/ApiContext' +import { CreateMemberForm } from 'src/views/credentials/CreateMemberForm' +import { apiValue as apiValueMock } from 'src/const/apiProviderMock' + +describe('ApiContext', () => { + const preloadedState = { + ...initialState, + connect: { + ...initialState.connect, + current_institution_guid: 'INS-123', + selectedInstitution: { + guid: 'INS-123', + code: 'mxbank', + name: 'MX Bank', + }, + institutions: [ + { + guid: 'INS-123', + code: 'mxbank', + name: 'MX Bank', + }, + ], + }, + } + + const defaultProps = { + onError: () => {}, + onSuccess: () => {}, + } + + it('provides API to child components', async () => { + const mockGetInstitutionCredentials = vi.fn().mockResolvedValue([ + { + guid: 'CRD-1', + label: 'Username', + field_name: 'username', + field_type: 'TEXT', + }, + ]) + + render( + + + , + { preloadedState }, + ) + + await waitFor(() => { + expect(mockGetInstitutionCredentials).toHaveBeenCalledWith('INS-123') + }) + + expect(screen.getByText('Username')).toBeInTheDocument() + }) + + it('allows custom API values to be provided', async () => { + const customGetInstitutionCredentials = vi.fn().mockResolvedValue([ + { + guid: 'CRD-2', + label: 'Password', + field_name: 'password', + field_type: 'PASSWORD', + }, + ]) + + render( + + + , + { preloadedState }, + ) + + await waitFor(() => { + expect(customGetInstitutionCredentials).toHaveBeenCalledWith('INS-123') + }) + + expect(screen.getByText('Password')).toBeInTheDocument() + }) + + it('provides default API values when used outside provider', () => { + const TestComponent = () => { + const { api } = useApi() + return ( +
+
{typeof api.loadMembers === 'function' ? 'yes' : 'no'}
+
+ ) + } + + render(, { preloadedState }) + + expect(screen.getByTestId('has-api')).toHaveTextContent('yes') + }) +}) diff --git a/src/context/ApiContext.tsx b/src/context/ApiContext.tsx index 0be06eccb8..540f8affc0 100644 --- a/src/context/ApiContext.tsx +++ b/src/context/ApiContext.tsx @@ -141,9 +141,6 @@ const ApiProvider = ({ apiValue, children }: ApiProviderTypes) => { const useApi = () => { const context = React.useContext(ApiContext) - if (context === undefined) { - throw new Error('useApi must be used within a ApiProvider') - } return { api: context } } diff --git a/src/context/WebSocketContext-test.tsx b/src/context/WebSocketContext-test.tsx new file mode 100644 index 0000000000..79c791d5fc --- /dev/null +++ b/src/context/WebSocketContext-test.tsx @@ -0,0 +1,74 @@ +import React from 'react' +import { renderHook } from '@testing-library/react' +import { of } from 'rxjs' +import { WebSocketProvider, useWebSocket, WebSocketConnection } from 'src/context/WebSocketContext' + +describe('WebSocketContext', () => { + it('should return undefined when no WebSocket connection is provided', () => { + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => {children}, + }) + + expect(result.current).toBeUndefined() + }) + + it('should return the WebSocket connection when provided', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } + + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), + }) + + expect(result.current).toBe(mockConnection) + expect(result.current?.isConnected()).toBe(true) + }) + + it('should allow accessing webSocketMessages$ observable', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({ event: 'test', payload: { id: 123 } }), + } + + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), + }) + + expect(result.current?.webSocketMessages$).toBeDefined() + + let receivedMessage: unknown + result.current?.webSocketMessages$.subscribe((msg) => { + receivedMessage = msg + }) + + expect(receivedMessage).toEqual({ event: 'test', payload: { id: 123 } }) + }) + + it('should provide the same connection to multiple consumers', () => { + const mockConnection: WebSocketConnection = { + isConnected: vi.fn(() => true), + webSocketMessages$: of({}), + } + + const { result: result1 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), + }) + + const { result: result2 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), + }) + + expect(result1.current).toBe(mockConnection) + expect(result2.current).toBe(mockConnection) + }) +}) diff --git a/src/main.tsx b/src/main.tsx deleted file mode 100644 index 0c5139e779..0000000000 --- a/src/main.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import './global.css' // Import foundational global styles -import './styles.css' // Import more specific global styles or overrides - -import React from 'react' -import { createRoot } from 'react-dom/client' -import ConnectWidget from './ConnectWidget' -import { AGG_MODE } from 'src/const/Connect' - -createRoot(document.getElementById('root') as HTMLElement).render( - , -) diff --git a/src/privacy/withProtection-test.tsx b/src/privacy/withProtection-test.tsx new file mode 100644 index 0000000000..9dd2963d46 --- /dev/null +++ b/src/privacy/withProtection-test.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { maskInputFn, withProtection } from 'src/privacy/withProtection' +import { render } from 'src/utilities/testingLibrary' + +describe('maskInputFn', () => { + it('should mask input text by default', () => { + const result = maskInputFn('password123') + expect(result).toBe('***********') + }) + + it('should return original text when element has data-ph-unmask="true"', () => { + const element = document.createElement('input') + element.setAttribute('data-ph-unmask', 'true') + const result = maskInputFn('plainText123', element) + expect(result).toBe('plainText123') + }) +}) + +describe('withProtection', () => { + it('should wrap component with ph-no-capture class by default', () => { + const TestComponent = ({ 'data-test': dataTest }: { 'data-test': string }) => ( +
Sensitive Content
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeTruthy() + expect(screen.getByTestId('test-component')).toHaveTextContent('Sensitive Content') + }) + + it('should not wrap component when allowCapture is true', () => { + const TestComponent = ({ 'data-test': dataTest }: { 'data-test': string }) => ( +
Public Content
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeNull() + expect(screen.getByTestId('test-component')).toHaveTextContent('Public Content') + }) + + it('should add data-ph-unmask attribute when allowCapture is true', () => { + const TestComponent = React.forwardRef< + HTMLInputElement, + { 'data-test': string; 'data-ph-unmask'?: boolean } + >((props, ref) => ) + TestComponent.displayName = 'TestComponent' + + const ProtectedComponent = withProtection(TestComponent) + + render() + + const input = screen.getByTestId('test-input') + expect(input.getAttribute('data-ph-unmask')).toBe('true') + }) +}) diff --git a/src/services/mockedData.ts b/src/services/mockedData.ts index a5f2bc3a9b..31cd3d9a33 100644 --- a/src/services/mockedData.ts +++ b/src/services/mockedData.ts @@ -304,6 +304,33 @@ export const memberCredentialsData = { }, ], } + +export const CONNECTED_MEMBER = { + guid: 'MBR-123', + name: 'Chase Bank', + connection_status: 6, + aggregation_status: 1, + institution_guid: 'INS-123', + user_guid: 'USR-123', + is_being_aggregated: false, + is_manual: false, + is_managed_by_user: true, + is_oauth: false, +} + +export const NON_CONNECTED_MEMBER = { + guid: 'MBR-456', + name: 'Wells Fargo', + connection_status: 1, + aggregation_status: 0, + institution_guid: 'INS-456', + user_guid: 'USR-123', + is_being_aggregated: false, + is_manual: false, + is_managed_by_user: true, + is_oauth: false, +} + export const CONNECTED_MEMBERS = [ { aggregation_status: 1, diff --git a/src/views/disclosure/Disclosure.js b/src/views/disclosure/Disclosure.js index 2030ccf153..ce68617efe 100644 --- a/src/views/disclosure/Disclosure.js +++ b/src/views/disclosure/Disclosure.js @@ -29,6 +29,7 @@ import { goToUrlLink } from 'src/utilities/global' export const Disclosure = React.forwardRef((_, disclosureRef) => { const containerRef = useRef(null) useAnalyticsPath(...PageviewInfo.CONNECT_DISCLOSURE) + const size = useSelector(getSize) const isSmall = size === 'small' const tokens = useTokens() const styles = getStyles(tokens, isSmall) @@ -37,7 +38,6 @@ export const Disclosure = React.forwardRef((_, disclosureRef) => { // Redux const { isInAggMode, isInTaxMode, isInVerifyMode } = useSelector(selectCurrentMode) const connectConfig = useSelector(selectConnectConfig) - const size = useSelector(getSize) const showExternalLinkPopup = useSelector( (state) => state.profiles.clientProfile.show_external_link_popup, ) diff --git a/styles.css b/styles.css deleted file mode 100644 index fcac5837e9..0000000000 --- a/styles.css +++ /dev/null @@ -1,8 +0,0 @@ -@font-face { - font-family: 'MaterialSymbolsRounded'; - font-display: block; - font-style: normal; - font-weight: 100 700; - src: url('https://s3.amazonaws.com/MD_Assets/fonts/icon/MaterialSymbolsRounded.woff2') - format('woff2'); -} diff --git a/vite.config.ts b/vite.config.ts index 6eb0e5c278..5bb716b9dd 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -92,5 +92,23 @@ export default defineConfig({ inline: ['@mxenabled/mx-icons'], }, }, + coverage: { + provider: 'v8', + reporter: ['text', 'json', 'html'], + exclude: [ + 'node_modules/', + 'src/testSetup.ts', + 'src/index.ts', + 'src/main.tsx', + '**/*.d.ts', + '**/*-{test,spec}.{js,ts,jsx,tsx}', + '**/__tests__/**', + '**/dist/**', + '.eslintrc.cjs', + 'vite.config.ts', + 'scripts/**', + '**/__mocks__/**', + ], + }, }, })