From 37f31b80e0fe5e4339b1d45a528c35cb98bfa4fd Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 10:28:57 -0600 Subject: [PATCH 01/18] added test to src --- .gitignore | 1 + package-lock.json | 682 ++++++++++++++++++++++++++++++--- package.json | 2 + src/__tests__/Connect-test.tsx | 264 +++++++++++++ src/__tests__/Main-test.tsx | 102 +++++ vite.config.ts | 18 + 6 files changed, 1026 insertions(+), 43 deletions(-) create mode 100644 src/__tests__/Connect-test.tsx create mode 100644 src/__tests__/Main-test.tsx 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/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/__tests__/Connect-test.tsx b/src/__tests__/Connect-test.tsx new file mode 100644 index 0000000000..792cc42dc7 --- /dev/null +++ b/src/__tests__/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/__tests__/Main-test.tsx b/src/__tests__/Main-test.tsx new file mode 100644 index 0000000000..419cb26929 --- /dev/null +++ b/src/__tests__/Main-test.tsx @@ -0,0 +1,102 @@ +import React from 'react' +import { beforeEach, describe, it, expect, vi } from 'vitest' +import { render, waitFor } from 'src/utilities/testingLibrary' +import { AGG_MODE } from 'src/const/Connect' +import ConnectWidget from '../ConnectWidget' + +describe('main.tsx entry point', () => { + const defaultProps = { + clientConfig: { connect: { mode: AGG_MODE } }, + profiles: {}, + userFeatures: {}, + language: { locale: 'en', localizedContent: {} }, + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('ConnectWidget initialization', () => { + it('renders ConnectWidget with aggregation mode config', async () => { + render() + + await waitFor(() => { + expect(document.querySelector('#connect-wrapper')).toBeInTheDocument() + }) + }) + + it('uses the correct default mode configuration', () => { + const expectedConfig = { connect: { mode: AGG_MODE } } + + expect(expectedConfig.connect.mode).toBe('aggregation') + }) + + it('renders without errors when using main.tsx config structure', () => { + expect(() => { + render() + }).not.toThrow() + }) + }) + + describe('DOM mounting', () => { + it('renders to a root element', () => { + const container = document.createElement('div') + container.id = 'root' + document.body.appendChild(container) + + render(, { container }) + + expect(container.children.length).toBeGreaterThan(0) + + document.body.removeChild(container) + }) + + it('mounts ConnectWidget successfully', () => { + const { container } = render() + + expect(container.firstChild).toBeInTheDocument() + }) + }) + + describe('configuration validation', () => { + it('accepts the main.tsx config format', () => { + const { container } = render() + + expect(container).toBeInTheDocument() + }) + + it('uses aggregation mode as specified in main.tsx', async () => { + render() + + await waitFor(() => { + expect(document.querySelector('#connect-wrapper')).toBeInTheDocument() + }) + }) + + it('renders the widget with correct mode constant', () => { + expect(AGG_MODE).toBe('aggregation') + + const { container } = render() + + expect(container).toBeTruthy() + }) + }) + + describe('React 18 compatibility', () => { + it('is compatible with React 18 createRoot API', () => { + expect(() => { + render() + }).not.toThrow() + }) + + it('renders without strict mode violations', () => { + const { container } = render( + + + , + ) + + expect(container).toBeInTheDocument() + }) + }) +}) 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__/**', + ], + }, }, }) From 3db3741fc6217a6c5c1737c289b12e1078b6cb30 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 10:33:16 -0600 Subject: [PATCH 02/18] fix: added src tests to bring coverage to 80% From 96ded32afb562cafe2301d4b726f7f3d76754427 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 11:20:10 -0600 Subject: [PATCH 03/18] fix: added components tests to increase coverage to 80% --- src/components/Container.js | 40 -- src/components/Container.tsx | 7 +- .../ConnectInstitutionHeader-test.tsx | 108 +++ src/components/__tests__/Container-test.tsx | 262 +++++++ .../__tests__/DeleteMemberSurvey-test.tsx | 345 ++++++++++ .../__tests__/DetailReviewItem-test.tsx | 253 +++++++ .../__tests__/RenderConnectStep-test.jsx | 648 +++++++++++++++++- src/services/mockedData.ts | 27 + src/views/disclosure/Disclosure.js | 2 +- 9 files changed, 1629 insertions(+), 63 deletions(-) delete mode 100644 src/components/Container.js create mode 100644 src/components/__tests__/ConnectInstitutionHeader-test.tsx create mode 100644 src/components/__tests__/Container-test.tsx create mode 100644 src/components/__tests__/DeleteMemberSurvey-test.tsx create mode 100644 src/components/__tests__/DetailReviewItem-test.tsx 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/__tests__/ConnectInstitutionHeader-test.tsx b/src/components/__tests__/ConnectInstitutionHeader-test.tsx new file mode 100644 index 0000000000..95c0b0e271 --- /dev/null +++ b/src/components/__tests__/ConnectInstitutionHeader-test.tsx @@ -0,0 +1,108 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render } 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, + }, + }) + + describe('rendering', () => { + it('renders the header container with correct data-test attribute', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + }) + + it('renders SVG elements for the header graphics', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + }) + + describe('color scheme', () => { + it('renders with light mode color scheme', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders with dark mode color scheme', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + }) + + describe('institution logo', () => { + it('renders with institutionGuid provided', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const institutionGuid = 'INS-12345' + + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders without institutionGuid', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + + it('renders with undefined institutionGuid', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="disclosure-svg-header"]')).toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders all elements together in light mode with institution', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.LIGHT) + const institutionGuid = 'INS-BANK-001' + + const { container } = render(, { + preloadedState, + }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + + it('renders all elements together in dark mode without institution', () => { + const preloadedState = createPreloadedState(COLOR_SCHEME.DARK) + + const { container } = render(, { preloadedState }) + + const header = container.querySelector('[data-test="disclosure-svg-header"]') + expect(header).toBeInTheDocument() + + const svgs = container.querySelectorAll('svg') + expect(svgs.length).toBeGreaterThan(0) + }) + }) +}) diff --git a/src/components/__tests__/Container-test.tsx b/src/components/__tests__/Container-test.tsx new file mode 100644 index 0000000000..3314dedb2d --- /dev/null +++ b/src/components/__tests__/Container-test.tsx @@ -0,0 +1,262 @@ +import React from 'react' +import { describe, it, expect } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { Container } from 'src/components/Container.tsx' +import { STEPS } from 'src/const/Connect' +import { initialState } from 'src/services/mockedData' + +describe('Container', () => { + const preloadedState = initialState + + describe('rendering', () => { + it('renders the container with correct data-test attribute', () => { + const { container } = render( + +
Test Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + + it('renders children content', () => { + render( + +
Test Content
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('child-content')).toBeInTheDocument() + expect(screen.getByText('Test Content')).toBeInTheDocument() + }) + + it('renders multiple children', () => { + render( + +
First Child
+
Second Child
+ Third Child +
, + { preloadedState }, + ) + + expect(screen.getByTestId('child-1')).toBeInTheDocument() + expect(screen.getByTestId('child-2')).toBeInTheDocument() + expect(screen.getByTestId('child-3')).toBeInTheDocument() + }) + + it('renders without children', () => { + const { container } = render(, { preloadedState }) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + + it('renders with null children', () => { + const { container } = render({null}, { preloadedState }) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + }) + }) + + describe('step prop', () => { + it('renders without step prop', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders with SEARCH step and applies maxHeight', () => { + const { container } = render( + +
Search Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders with non-SEARCH step without maxHeight constraint', () => { + const { container } = render( + +
Connected Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).not.toHaveStyle({ maxHeight: '100%' }) + }) + + it('renders correctly with ENTER_CREDENTIALS step', () => { + render( + +
Enter Credentials
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('credentials-content')).toBeInTheDocument() + }) + + it('renders correctly with MFA step', () => { + render( + +
MFA Content
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('mfa-content')).toBeInTheDocument() + }) + }) + + describe('styling', () => { + it('applies consistent container styles', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ + minHeight: '100%', + display: 'flex', + justifyContent: 'center', + }) + }) + + it('has a content wrapper with proper constraints', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + const contentWrapper = containerDiv?.firstChild as HTMLElement + + expect(contentWrapper).toBeInTheDocument() + expect(contentWrapper).toHaveStyle({ + maxWidth: '400px', + minWidth: '270px', + width: '100%', + }) + }) + + it('applies background color from tokens', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ backgroundColor: expect.any(String) }) + }) + }) + + describe('integration', () => { + it('renders complete structure with SEARCH step', () => { + const { container } = render( + +
Search for institution
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) + expect(screen.getByTestId('search-content')).toBeInTheDocument() + expect(screen.getByText('Search for institution')).toBeInTheDocument() + }) + + it('renders complete structure with CONNECTED step', () => { + const { container } = render( + +
Successfully connected!
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(screen.getByTestId('success-message')).toBeInTheDocument() + expect(screen.getByText('Successfully connected!')).toBeInTheDocument() + }) + + it('renders nested component structure', () => { + render( + +
+

Title

+
+

Nested Content

+
+
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('outer')).toBeInTheDocument() + expect(screen.getByTestId('inner')).toBeInTheDocument() + expect(screen.getByText('Title')).toBeInTheDocument() + expect(screen.getByText('Nested Content')).toBeInTheDocument() + }) + + it('maintains structure with form elements', () => { + render( + +
+ + +
+
, + { preloadedState }, + ) + + expect(screen.getByTestId('test-form')).toBeInTheDocument() + expect(screen.getByTestId('test-input')).toBeInTheDocument() + expect(screen.getByTestId('test-button')).toBeInTheDocument() + }) + + it('wraps components consistently regardless of content type', () => { + const { container } = render( + +
+ Text + test + +
+
, + { preloadedState }, + ) + + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toBeInTheDocument() + expect(screen.getByText('Text')).toBeInTheDocument() + expect(screen.getByAltText('test')).toBeInTheDocument() + expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/__tests__/DeleteMemberSurvey-test.tsx b/src/components/__tests__/DeleteMemberSurvey-test.tsx new file mode 100644 index 0000000000..3e44e1c844 --- /dev/null +++ b/src/components/__tests__/DeleteMemberSurvey-test.tsx @@ -0,0 +1,345 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen, waitFor } from 'src/utilities/testingLibrary' +import { DeleteMemberSurvey } from 'src/components/DeleteMemberSurvey' +import { initialState, CONNECTED_MEMBER, NON_CONNECTED_MEMBER } from 'src/services/mockedData' +import userEvent from '@testing-library/user-event' + +describe('DeleteMemberSurvey', () => { + const preloadedState = initialState + + const mockOnCancel = vi.fn() + const mockOnDeleteSuccess = vi.fn() + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('rendering', () => { + it('renders the disconnect institution dialog', () => { + const { container } = render( + , + { preloadedState }, + ) + + const dialog = container.querySelector('[role="dialog"]') + expect(dialog).toBeInTheDocument() + }) + + it('renders the disconnect institution heading', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + }) + + it('renders the disclaimer with member name', () => { + render( + , + { preloadedState }, + ) + + const disclaimer = screen.getByTestId('disconnect-disclaimer') + expect(disclaimer).toBeInTheDocument() + expect(disclaimer.textContent).toContain('Chase Bank') + }) + + it('renders disconnect and cancel buttons', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + }) + + it('renders required field indicator', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Required')).toBeInTheDocument() + }) + }) + + describe('connected member reasons', () => { + it('renders correct reasons for connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() + expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() + expect(screen.getByText("I don't want to use this app")).toBeInTheDocument() + expect(screen.getByText('Other')).toBeInTheDocument() + }) + + it('does not render non-connected reasons for connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() + expect( + screen.queryByText('The account information is old or inaccurate'), + ).not.toBeInTheDocument() + expect(screen.queryByText("I don't want this account connected here")).not.toBeInTheDocument() + }) + }) + + describe('non-connected member reasons', () => { + it('renders correct reasons for non-connected member', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() + expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() + expect(screen.getByText("I don't want this account connected here")).toBeInTheDocument() + expect(screen.getByText('Other')).toBeInTheDocument() + }) + + it('does not render connected-only reasons for non-connected member', () => { + render( + , + { preloadedState }, + ) + + expect( + screen.queryByText("I no longer use this account or it's not mine"), + ).not.toBeInTheDocument() + expect(screen.queryByText("I don't want to share my data")).not.toBeInTheDocument() + expect(screen.queryByText("I don't want to use this app")).not.toBeInTheDocument() + }) + }) + + describe('user interactions', () => { + it('calls onCancel when cancel button is clicked', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-cancel-button')) + + expect(mockOnCancel).toHaveBeenCalledTimes(1) + }) + + it('allows selecting a reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + expect(options[0]).toBeChecked() + }) + + it('allows changing selected reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + expect(options[0]).toBeChecked() + + await user.click(options[1]) + expect(options[1]).toBeChecked() + expect(options[0]).not.toBeChecked() + }) + }) + + describe('form validation', () => { + it('shows validation error when disconnect clicked without selecting reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + }) + + it('does not show validation error before first submit attempt', () => { + render( + , + { preloadedState }, + ) + + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + + it('validation error disappears after selecting a reason', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + await user.click(screen.getByTestId('disconnect-button')) + + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() + }) + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + await waitFor(() => { + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + }) + }) + + describe('delete member flow', () => { + it('initiates delete when disconnect clicked with valid selection', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + + await user.click(screen.getByTestId('disconnect-button')) + + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders complete structure for connected member', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-disclaimer')).toBeInTheDocument() + expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) + expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + }) + + it('renders complete structure for non-connected member', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() + expect(screen.getByText('Disconnect institution')).toBeInTheDocument() + expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Wells Fargo') + expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) + }) + + it('handles complete user flow from selection to cancel', async () => { + const user = userEvent.setup() + render( + , + { preloadedState }, + ) + + const options = screen.getAllByRole('radio') + await user.click(options[0]) + expect(options[0]).toBeChecked() + + await user.click(screen.getByTestId('disconnect-cancel-button')) + expect(mockOnCancel).toHaveBeenCalledTimes(1) + }) + }) +}) diff --git a/src/components/__tests__/DetailReviewItem-test.tsx b/src/components/__tests__/DetailReviewItem-test.tsx new file mode 100644 index 0000000000..6ff3d4bed9 --- /dev/null +++ b/src/components/__tests__/DetailReviewItem-test.tsx @@ -0,0 +1,253 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import { DetailReviewItem } from 'src/components/DetailReviewItem' +import { initialState } from 'src/services/mockedData' +import userEvent from '@testing-library/user-event' + +describe('DetailReviewItem', () => { + const preloadedState = initialState + + const defaultProps = { + label: 'Email', + value: 'user@example.com', + ariaButtonLabel: 'Edit email', + isEditable: false, + onEditClick: vi.fn(), + } + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('rendering', () => { + it('renders the label', () => { + render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + }) + + it('renders the value', () => { + render(, { preloadedState }) + + expect(screen.getByText('user@example.com')).toBeInTheDocument() + }) + + it('renders with correct data-test attributes for label', () => { + const { container } = render(, { preloadedState }) + + const labelElement = container.querySelector('[data-test="Email-row"]') + expect(labelElement).toBeInTheDocument() + }) + + it('renders with correct data-test attributes for value', () => { + const { container } = render(, { preloadedState }) + + const valueElement = container.querySelector('[data-test="user@example.com-row"]') + expect(valueElement).toBeInTheDocument() + }) + + it('renders edit button with correct aria-label', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeInTheDocument() + }) + + it('renders edit icon', () => { + const { container } = render(, { preloadedState }) + + expect(container.querySelector('[data-test="Email-edit-button"]')).toBeInTheDocument() + }) + + it('sanitizes label with spaces for data-test attribute', () => { + const { container } = render(, { + preloadedState, + }) + + expect(container.querySelector('[data-test="Full-Name-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="Full-Name-edit-button"]')).toBeInTheDocument() + }) + }) + + describe('edit button functionality', () => { + it('calls onEditClick when edit button is clicked', async () => { + const user = userEvent.setup() + const mockOnEditClick = vi.fn() + + render(, { + preloadedState, + }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + await user.click(button) + + expect(mockOnEditClick).toHaveBeenCalledTimes(1) + }) + + it('enables edit button when isEditable is false', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeEnabled() + }) + + it('disables edit button when isEditable is true', () => { + render(, { preloadedState }) + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeDisabled() + }) + }) + + describe('different content types', () => { + it('renders with phone number', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Phone')).toBeInTheDocument() + expect(screen.getByText('555-123-4567')).toBeInTheDocument() + }) + + it('renders with address', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Address')).toBeInTheDocument() + expect(screen.getByText('123 Main St, City, ST 12345')).toBeInTheDocument() + }) + + it('renders with date', () => { + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Date of Birth')).toBeInTheDocument() + expect(screen.getByText('01/01/1990')).toBeInTheDocument() + }) + + it('renders with long text value', () => { + const longValue = + 'This is a very long value that might wrap to multiple lines depending on the container width' + + render( + , + { preloadedState }, + ) + + expect(screen.getByText('Description')).toBeInTheDocument() + expect(screen.getByText(longValue)).toBeInTheDocument() + }) + }) + + describe('data-test attribute handling', () => { + it('handles special characters in label', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[data-test="First-&-Last-Name-row"]')).toBeInTheDocument() + expect( + container.querySelector('[data-test="First-&-Last-Name-edit-button"]'), + ).toBeInTheDocument() + }) + + it('handles special characters in value', () => { + const { container } = render( + , + { preloadedState }, + ) + + expect(container.querySelector('[data-test="user+test@example.com-row"]')).toBeInTheDocument() + }) + }) + + describe('integration', () => { + it('renders complete structure with all elements', () => { + const { container } = render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + + expect(screen.getByText('user@example.com')).toBeInTheDocument() + + expect(screen.getByRole('button', { name: 'Edit email' })).toBeInTheDocument() + + expect(container.querySelector('[data-test="Email-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="user@example.com-row"]')).toBeInTheDocument() + expect(container.querySelector('[data-test="Email-edit-button"]')).toBeInTheDocument() + }) + + it('handles complete user interaction flow', async () => { + const user = userEvent.setup() + const mockOnEditClick = vi.fn() + + render(, { + preloadedState, + }) + + expect(screen.getByText('Email')).toBeInTheDocument() + expect(screen.getByText('user@example.com')).toBeInTheDocument() + + const button = screen.getByRole('button', { name: 'Edit email' }) + expect(button).toBeEnabled() + + await user.click(button) + + expect(mockOnEditClick).toHaveBeenCalledTimes(1) + }) + + it('renders correctly with multiple items scenario', () => { + const { rerender } = render(, { preloadedState }) + + expect(screen.getByText('Email')).toBeInTheDocument() + + rerender( + , + ) + + expect(screen.getByText('Phone')).toBeInTheDocument() + expect(screen.getByText('555-1234')).toBeInTheDocument() + }) + }) +}) diff --git a/src/components/__tests__/RenderConnectStep-test.jsx b/src/components/__tests__/RenderConnectStep-test.jsx index 6207b671f2..dbebc59c14 100644 --- a/src/components/__tests__/RenderConnectStep-test.jsx +++ b/src/components/__tests__/RenderConnectStep-test.jsx @@ -1,16 +1,19 @@ import React from 'react' import { render, screen } from 'src/utilities/testingLibrary' import RenderConnectStep from 'src/components/RenderConnectStep' -import { STEPS } from 'src/const/Connect' +import { VERIFY_MODE, STEPS } from 'src/const/Connect' import { createRenderConnectStepInitialState } from 'src/utilities/test/createRenderConnectStepInitialState' +import { initialState } from 'src/services/mockedData' describe('RenderConnectStep', () => { const defaultProps = { availableAccountTypes: [], handleConsentGoBack: vi.fn(), handleCredentialsGoBack: vi.fn(), + handleOAuthGoBack: vi.fn(), navigationRef: React.createRef(), onManualAccountAdded: vi.fn(), + onSuccessfulAggregation: vi.fn(), onUpsertMember: vi.fn(), setConnectLocalState: vi.fn(), } @@ -23,30 +26,635 @@ describe('RenderConnectStep', () => { url: 'https://testbank.com', } - it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { - const initialState = createRenderConnectStepInitialState( - STEPS.DEMO_CONNECT_GUARD, - mockInstitution, - ) + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Step Rendering', () => { + it('should render DemoConnectGuard when step is DEMO_CONNECT_GUARD', () => { + const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + expect(screen.getByText('Demo mode active')).toBeInTheDocument() + expect( + screen.getByText(/Live institutions are not available in the demo environment/i), + ).toBeInTheDocument() + expect(screen.getByText('MX Bank')).toBeInTheDocument() + + const logo = screen.getByAltText('Logo for Test Bank') + expect(logo).toBeInTheDocument() + expect(logo).toHaveAttribute('src', mockInstitution.logo_url) + + const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') + expect(errorIcon).toBeInTheDocument() + + const button = screen.getByRole('button', { name: /return to institution selection/i }) + expect(button).toBeInTheDocument() + }) + + it('should render Search view for SEARCH step', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render Connecting view for CONNECTING step', () => { + const state = createRenderConnectStepInitialState(STEPS.CONNECTING, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render Disclosure view for DISCLOSURE step', () => { + const state = createRenderConnectStepInitialState(STEPS.DISCLOSURE) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render InstitutionStatusDetails for INSTITUTION_STATUS_DETAILS step', () => { + const state = createRenderConnectStepInitialState( + STEPS.INSTITUTION_STATUS_DETAILS, + mockInstitution, + ) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render DynamicDisclosure for CONSENT step', () => { + const state = createRenderConnectStepInitialState(STEPS.CONSENT, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render ManualAccountConnect for ADD_MANUAL_ACCOUNT step', () => { + const state = createRenderConnectStepInitialState(STEPS.ADD_MANUAL_ACCOUNT) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render MFAStep for MFA step', () => { + const state = createRenderConnectStepInitialState(STEPS.MFA, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render VerifyExistingMember for VERIFY_EXISTING_MEMBER step', () => { + const state = createRenderConnectStepInitialState(STEPS.VERIFY_EXISTING_MEMBER) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render VerifyError for VERIFY_ERROR step', () => { + const state = createRenderConnectStepInitialState(STEPS.VERIFY_ERROR) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it.skip('should render Connected for CONNECTED step', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member' } + const state = { + ...createRenderConnectStepInitialState(STEPS.CONNECTED, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.CONNECTED, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + // Just verify it renders without error - confetti testing is in Connected component tests + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render DeleteMemberSuccess for DELETE_MEMBER_SUCCESS step', () => { + const state = createRenderConnectStepInitialState( + STEPS.DELETE_MEMBER_SUCCESS, + mockInstitution, + ) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render OAuthError for OAUTH_ERROR step', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member' } + const state = { + ...createRenderConnectStepInitialState(STEPS.OAUTH_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.OAUTH_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should default to SEARCH step when location is empty', () => { + const state = { + ...initialState, + connect: { + ...initialState.connect, + location: [], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Styling', () => { + it('should apply maxHeight for SEARCH step', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv).toHaveStyle({ + maxHeight: 'calc(100% - 60px)', + }) + }) + + it('should not apply maxHeight for non-SEARCH steps', () => { + const state = createRenderConnectStepInitialState(STEPS.DEMO_CONNECT_GUARD, mockInstitution) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv.style.maxHeight).toBe('') + }) + + it('should apply correct container styles', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const containerDiv = container.firstChild + expect(containerDiv).toHaveStyle({ + display: 'flex', + justifyContent: 'center', + minHeight: 'calc(100% - 60px)', + }) + }) + }) + + describe('Configuration-Dependent Rendering', () => { + it('should render in AGG_MODE by default', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) - const { container } = render(, { - preloadedState: initialState, + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render in VERIFY_MODE when configured', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.SEARCH), + config: { + ...initialState.config, + mode: VERIFY_MODE, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should apply widget profile settings', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.SEARCH), + profiles: { + ...initialState.profiles, + widgetProfile: { + ...initialState.profiles.widgetProfile, + enable_support_requests: false, + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Integration', () => { + it('should render complete component with all providers', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should handle step navigation', () => { + const state1 = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container, rerender } = render(, { + preloadedState: state1, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + + rerender() }) - expect(screen.getByText('Demo mode active')).toBeInTheDocument() - expect( - screen.getByText(/Live institutions are not available in the demo environment/i), - ).toBeInTheDocument() - expect(screen.getByText('MX Bank')).toBeInTheDocument() + it('should pass props correctly to views', () => { + const state = createRenderConnectStepInitialState(STEPS.SEARCH) - const logo = screen.getByAltText('Logo for Test Bank') - expect(logo).toBeInTheDocument() - expect(logo).toHaveAttribute('src', mockInstitution.logo_url) + const { container } = render(, { + preloadedState: state, + }) - const errorIcon = container.querySelector('svg.MuiSvgIcon-colorError') - expect(errorIcon).toBeInTheDocument() + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should handle missing optional props gracefully', () => { + const minimalProps = { + handleConsentGoBack: vi.fn(), + handleCredentialsGoBack: vi.fn(), + handleOAuthGoBack: vi.fn(), + navigationRef: React.createRef(), + setConnectLocalState: vi.fn(), + } + + const state = createRenderConnectStepInitialState(STEPS.SEARCH) + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('Error Handling', () => { + it('should handle invalid step gracefully', () => { + const state = { + ...initialState, + connect: { + ...initialState.connect, + location: [{ step: 'INVALID_STEP' }], + }, + } + + render(, { + preloadedState: state, + }) + expect(true).toBe(true) + }) + }) + + describe('ENTER_CREDENTIALS Step Variations', () => { + it.skip('should render OAuthStep when institution supports OAuth', () => { + const oauthInstitution = { ...mockInstitution, supports_oauth: true } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, oauthInstitution), + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + uses_oauth: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, oauthInstitution).connect, + selectedInstitution: oauthInstitution, + updateCredentials: false, + selectedInstructionalData: { + title: 'Log in at Test Bank', + description: 'Connect your account', + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render UpdateMemberForm when updateCredentials is true', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + updateCredentials: true, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) - const button = screen.getByRole('button', { name: /return to institution selection/i }) - expect(button).toBeInTheDocument() + it('should render CreateMemberForm when updateCredentials is false', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + updateCredentials: false, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it.skip('should render OAuthStep when current member is OAuth', () => { + const mockMember = { guid: 'MEM-123', name: 'Test Member', is_oauth: true } + const state = { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution), + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + uses_oauth: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.ENTER_CREDENTIALS, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + updateCredentials: false, + selectedInstructionalData: { + title: 'Log in at Test Bank', + description: 'Connect your account', + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('MICRODEPOSITS Step', () => { + it('should render Microdeposits when enabled in verification mode', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS), + config: { + ...initialState.config, + mode: VERIFY_MODE, + }, + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + account_verification_is_enabled: true, + is_microdeposits_enabled: true, + }, + widgetProfile: { + ...initialState.profiles.widgetProfile, + show_microdeposits_in_connect: true, + }, + }, + connect: { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS).connect, + currentMicrodepositGuid: 'MICRO-123', + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should not render Microdeposits when not enabled', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.MICRODEPOSITS), + config: { + ...initialState.config, + mode: 'aggregation', + }, + profiles: { + ...initialState.profiles, + clientProfile: { + ...initialState.profiles.clientProfile, + account_verification_is_enabled: false, + is_microdeposits_enabled: false, + }, + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('ACTIONABLE_ERROR Step Variations', () => { + it('should render ActionableError when error code can be handled', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: { error_code: 'REQUEST_EXPIRED' }, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render LoginError when error code cannot be handled', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: { error_code: 'UNKNOWN_ERROR' }, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should render LoginError when error code is null', () => { + const mockMember = { + guid: 'MEM-123', + name: 'Test Member', + connection_status: 'PREVENTED', + error: null, + } + const state = { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution), + connect: { + ...createRenderConnectStepInitialState(STEPS.ACTIONABLE_ERROR, mockInstitution).connect, + currentMemberGuid: mockMember.guid, + members: [mockMember], + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + }) + + describe('ADDITIONAL_PRODUCT Step', () => { + it('should render AdditionalProductStep with valid product option', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ADDITIONAL_PRODUCT, mockInstitution), + config: { + ...initialState.config, + additional_product_option: 'account_verification', + }, + } + + const { container } = render(, { + preloadedState: state, + }) + + const stepWrapper = container.firstChild + expect(stepWrapper).toBeInTheDocument() + }) + + it('should throw error for invalid product option', () => { + const state = { + ...createRenderConnectStepInitialState(STEPS.ADDITIONAL_PRODUCT), + config: { + ...initialState.config, + additional_product_option: 'invalid_option', + }, + } + + expect(() => { + render(, { + preloadedState: state, + }) + }).toThrow('invalid product offer') + }) }) }) 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, ) From 7e1fb7efc9a1fff122f946d2abe3a286a040c59c Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 11:48:49 -0600 Subject: [PATCH 04/18] fix: add app, context, and privacy tests --- .../__tests__/IEDeprecationDialog-test.tsx | 300 ++++++++++++++ src/const/__tests__/Accounts-test.tsx | 162 ++++++++ src/const/__tests__/jobDetailCode-test.tsx | 51 +++ src/context/__tests__/ApiContext-test.tsx | 382 ++++++++++++++++++ .../__tests__/WebSocketContext-test.tsx | 208 ++++++++++ src/privacy/__tests__/withProtection-test.tsx | 230 +++++++++++ 6 files changed, 1333 insertions(+) create mode 100644 src/components/app/__tests__/IEDeprecationDialog-test.tsx create mode 100644 src/const/__tests__/Accounts-test.tsx create mode 100644 src/const/__tests__/jobDetailCode-test.tsx create mode 100644 src/context/__tests__/ApiContext-test.tsx create mode 100644 src/context/__tests__/WebSocketContext-test.tsx create mode 100644 src/privacy/__tests__/withProtection-test.tsx diff --git a/src/components/app/__tests__/IEDeprecationDialog-test.tsx b/src/components/app/__tests__/IEDeprecationDialog-test.tsx new file mode 100644 index 0000000000..bb03ec6687 --- /dev/null +++ b/src/components/app/__tests__/IEDeprecationDialog-test.tsx @@ -0,0 +1,300 @@ +import React from 'react' +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { render, screen } from 'src/utilities/testingLibrary' +import userEvent from '@testing-library/user-event' +import { initialState } from 'src/services/mockedData' +import { IEDeprecationDialog } from '../IEDeprecationDialog' +import { PageviewInfo } from 'src/const/Analytics' +import { isIE } from 'src/utilities/Browser' +import type { RootState } from 'src/redux/Store' + +vi.mock('src/utilities/Browser') + +describe('IEDeprecationDialog', () => { + const mockOnAnalyticPageview = vi.fn() + + const defaultProps = { + onAnalyticPageview: mockOnAnalyticPageview, + } + + const preloadedState = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: true, + }, + }, + } as unknown as Partial + + beforeEach(() => { + vi.clearAllMocks() + }) + + describe('Rendering', () => { + it('renders dialog when isIE is true and feature flag is enabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + }) + + it('does not render when isIE is false', () => { + vi.mocked(isIE).mockReturnValue(false) + + render(, { preloadedState }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('does not render when feature flag is disabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutFlag = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: false, + }, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutFlag, + }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('does not render when widgetProfile is null', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutProfile = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: null, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutProfile, + }) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('renders all text content', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + expect(screen.getByText(/We no longer support Internet Explorer/i)).toBeInTheDocument() + expect(screen.getByText('Continue')).toBeInTheDocument() + expect(screen.getByText(/Clicking the links to supported browsers/i)).toBeInTheDocument() + }) + + it('renders browser links with correct hrefs', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + const edgeLink = screen.getByText('Edge').closest('a') + const chromeLink = screen.getByText('Chrome').closest('a') + const firefoxLink = screen.getByText('Firefox').closest('a') + + expect(edgeLink).toHaveAttribute('href', 'https://www.microsoft.com/edge') + expect(edgeLink).toHaveAttribute('target', '_blank') + expect(edgeLink).toHaveAttribute('rel', 'noreferrer noopener') + + expect(chromeLink).toHaveAttribute('href', 'https://www.google.com/chrome/') + expect(chromeLink).toHaveAttribute('target', '_blank') + expect(chromeLink).toHaveAttribute('rel', 'noreferrer noopener') + + expect(firefoxLink).toHaveAttribute('href', 'https://www.mozilla.org/firefox/') + expect(firefoxLink).toHaveAttribute('target', '_blank') + expect(firefoxLink).toHaveAttribute('rel', 'noreferrer noopener') + }) + + it('renders close button with correct aria-label', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + expect(closeButton).toBeInTheDocument() + }) + }) + + describe('User Interactions', () => { + it('hides dialog when close button is clicked', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + await user.click(closeButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('hides dialog when continue button is clicked', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + const continueButton = screen.getByRole('button', { name: /continue/i }) + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + await user.click(continueButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + + it('keeps dialog hidden after being closed', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + const { rerender } = render(, { preloadedState }) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + await user.click(closeButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + + rerender() + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + }) + }) + + describe('Analytics', () => { + it('tracks pageview when dialog is shown', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).toHaveBeenCalledWith(PageviewInfo.CONNECT_IE_11_DEPRECATION[1]) + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + + it('does not track pageview when not IE', () => { + vi.mocked(isIE).mockReturnValue(false) + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).not.toHaveBeenCalled() + }) + + it('does not track pageview when feature flag is disabled', () => { + vi.mocked(isIE).mockReturnValue(true) + + const stateWithoutFlag = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: false, + }, + }, + } as unknown as Partial + + render(, { + preloadedState: stateWithoutFlag, + }) + + expect(mockOnAnalyticPageview).not.toHaveBeenCalled() + }) + + it('does not track pageview after dialog is closed', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + + const closeButton = screen.getByRole('button', { name: /close modal/i }) + await user.click(closeButton) + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + }) + + describe('Integration', () => { + it('renders complete dialog structure with all elements', () => { + vi.mocked(isIE).mockReturnValue(true) + + render(, { preloadedState }) + + expect(screen.getByRole('button', { name: /close modal/i })).toBeInTheDocument() + expect(screen.getByRole('button', { name: /continue/i })).toBeInTheDocument() + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + expect(screen.getByText('Edge')).toBeInTheDocument() + expect(screen.getByText('Chrome')).toBeInTheDocument() + expect(screen.getByText('Firefox')).toBeInTheDocument() + }) + + it('handles full user interaction flow', async () => { + vi.mocked(isIE).mockReturnValue(true) + const user = userEvent.setup() + + render(, { preloadedState }) + + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + + expect(mockOnAnalyticPageview).toHaveBeenCalledWith(PageviewInfo.CONNECT_IE_11_DEPRECATION[1]) + + const continueButton = screen.getByRole('button', { name: /continue/i }) + await user.click(continueButton) + + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + + expect(mockOnAnalyticPageview).toHaveBeenCalledTimes(1) + }) + + it('respects all conditional rendering flags', () => { + const testCases = [ + { isIE: false, flag: false, shouldRender: false }, + { isIE: false, flag: true, shouldRender: false }, + { isIE: true, flag: false, shouldRender: false }, + { isIE: true, flag: true, shouldRender: true }, + ] + + testCases.forEach(({ isIE: ieValue, flag, shouldRender }) => { + vi.mocked(isIE).mockReturnValue(ieValue) + + const testState = { + ...initialState, + profiles: { + ...initialState.profiles, + widgetProfile: { + enable_ie_11_deprecation: flag, + }, + }, + } as unknown as Partial + + const { unmount } = render(, { + preloadedState: testState, + }) + + if (shouldRender) { + expect(screen.getByText('This browser is not supported')).toBeInTheDocument() + } else { + expect(screen.queryByText('This browser is not supported')).not.toBeInTheDocument() + } + + unmount() + vi.clearAllMocks() + }) + }) + }) +}) diff --git a/src/const/__tests__/Accounts-test.tsx b/src/const/__tests__/Accounts-test.tsx new file mode 100644 index 0000000000..86001eb95f --- /dev/null +++ b/src/const/__tests__/Accounts-test.tsx @@ -0,0 +1,162 @@ +import { AccountTypeNames, ReadableAccountTypes } from '../Accounts' + +describe('Accounts Constants', () => { + describe('ReadableAccountTypes', () => { + it('should have UNKNOWN as 0', () => { + expect(ReadableAccountTypes.UNKNOWN).toBe(0) + }) + + it('should have CHECKING as 1', () => { + expect(ReadableAccountTypes.CHECKING).toBe(1) + }) + + it('should have SAVINGS as 2', () => { + expect(ReadableAccountTypes.SAVINGS).toBe(2) + }) + + it('should have LOAN as 3', () => { + expect(ReadableAccountTypes.LOAN).toBe(3) + }) + + it('should have CREDIT_CARD as 4', () => { + expect(ReadableAccountTypes.CREDIT_CARD).toBe(4) + }) + + it('should have INVESTMENT as 5', () => { + expect(ReadableAccountTypes.INVESTMENT).toBe(5) + }) + + it('should have LINE_OF_CREDIT as 6', () => { + expect(ReadableAccountTypes.LINE_OF_CREDIT).toBe(6) + }) + + it('should have MORTGAGE as 7', () => { + expect(ReadableAccountTypes.MORTGAGE).toBe(7) + }) + + it('should have PROPERTY as 8', () => { + expect(ReadableAccountTypes.PROPERTY).toBe(8) + }) + + it('should have CASH as 9', () => { + expect(ReadableAccountTypes.CASH).toBe(9) + }) + + it('should have INSURANCE as 10', () => { + expect(ReadableAccountTypes.INSURANCE).toBe(10) + }) + + it('should have PREPAID as 11', () => { + expect(ReadableAccountTypes.PREPAID).toBe(11) + }) + + it('should have CHECKING_LINE_OF_CREDIT as 12', () => { + expect(ReadableAccountTypes.CHECKING_LINE_OF_CREDIT).toBe(12) + }) + + it('should have exactly 13 account types', () => { + expect(Object.keys(ReadableAccountTypes)).toHaveLength(13) + }) + + it('should have all numeric values', () => { + Object.values(ReadableAccountTypes).forEach((value) => { + expect(typeof value).toBe('number') + }) + }) + + it('should have unique values', () => { + const values = Object.values(ReadableAccountTypes) + const uniqueValues = new Set(values) + expect(uniqueValues.size).toBe(values.length) + }) + }) + + describe('AccountTypeNames', () => { + it('should have 13 account type names', () => { + expect(AccountTypeNames).toHaveLength(13) + }) + + it('should have "Other" at index 0 for UNKNOWN', () => { + expect(AccountTypeNames[ReadableAccountTypes.UNKNOWN]).toBe('Other') + }) + + it('should have "Checking" at index 1 for CHECKING', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') + }) + + it('should have "Savings" at index 2 for SAVINGS', () => { + expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') + }) + + it('should have "Loan" at index 3 for LOAN', () => { + expect(AccountTypeNames[ReadableAccountTypes.LOAN]).toBe('Loan') + }) + + it('should have "Credit Card" at index 4 for CREDIT_CARD', () => { + expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') + }) + + it('should have "Investment" at index 5 for INVESTMENT', () => { + expect(AccountTypeNames[ReadableAccountTypes.INVESTMENT]).toBe('Investment') + }) + + it('should have "Line of Credit" at index 6 for LINE_OF_CREDIT', () => { + expect(AccountTypeNames[ReadableAccountTypes.LINE_OF_CREDIT]).toBe('Line of Credit') + }) + + it('should have "Mortgage" at index 7 for MORTGAGE', () => { + expect(AccountTypeNames[ReadableAccountTypes.MORTGAGE]).toBe('Mortgage') + }) + + it('should have "Property" at index 8 for PROPERTY', () => { + expect(AccountTypeNames[ReadableAccountTypes.PROPERTY]).toBe('Property') + }) + + it('should have "Cash" at index 9 for CASH', () => { + expect(AccountTypeNames[ReadableAccountTypes.CASH]).toBe('Cash') + }) + + it('should have "Insurance" at index 10 for INSURANCE', () => { + expect(AccountTypeNames[ReadableAccountTypes.INSURANCE]).toBe('Insurance') + }) + + it('should have "Prepaid" at index 11 for PREPAID', () => { + expect(AccountTypeNames[ReadableAccountTypes.PREPAID]).toBe('Prepaid') + }) + + it('should have "Checking" at index 12 for CHECKING_LINE_OF_CREDIT', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT]).toBe('Checking') + }) + + it('should have all string values', () => { + AccountTypeNames.forEach((name) => { + expect(typeof name).toBe('string') + }) + }) + }) + + describe('Integration between ReadableAccountTypes and AccountTypeNames', () => { + it('should map all ReadableAccountTypes to valid AccountTypeNames', () => { + Object.entries(ReadableAccountTypes).forEach(([_key, value]) => { + expect(AccountTypeNames[value]).toBeDefined() + expect(typeof AccountTypeNames[value]).toBe('string') + }) + }) + + it('should have correct mapping for UNKNOWN type', () => { + const name = AccountTypeNames[ReadableAccountTypes.UNKNOWN] + expect(name).toBe('Other') + }) + + it('should have correct mapping for standard account types', () => { + expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') + expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') + expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') + }) + + it('should handle CHECKING_LINE_OF_CREDIT as Checking', () => { + const name = AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT] + expect(name).toBe('Checking') + }) + }) +}) diff --git a/src/const/__tests__/jobDetailCode-test.tsx b/src/const/__tests__/jobDetailCode-test.tsx new file mode 100644 index 0000000000..017071d0b3 --- /dev/null +++ b/src/const/__tests__/jobDetailCode-test.tsx @@ -0,0 +1,51 @@ +import { JOB_DETAIL_CODE } from '../jobDetailCode' + +describe('JOB_DETAIL_CODE Constants', () => { + describe('Structure', () => { + it('should be an object', () => { + expect(typeof JOB_DETAIL_CODE).toBe('object') + expect(JOB_DETAIL_CODE).not.toBeNull() + }) + + it('should have exactly 1 property', () => { + expect(Object.keys(JOB_DETAIL_CODE)).toHaveLength(1) + }) + + it('should have all numeric values', () => { + Object.values(JOB_DETAIL_CODE).forEach((value) => { + expect(typeof value).toBe('number') + }) + }) + + it('should have unique values', () => { + const values = Object.values(JOB_DETAIL_CODE) + const uniqueValues = new Set(values) + expect(uniqueValues.size).toBe(values.length) + }) + }) + + describe('NO_VERIFIABLE_ACCOUNTS', () => { + it('should exist', () => { + expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBeDefined() + }) + + it('should equal 1000', () => { + expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe(1000) + }) + + it('should be a number', () => { + expect(typeof JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe('number') + }) + }) + + describe('Export', () => { + it('should export JOB_DETAIL_CODE as a named export', () => { + expect(JOB_DETAIL_CODE).toBeDefined() + }) + + it('should not be frozen or sealed', () => { + expect(Object.isFrozen(JOB_DETAIL_CODE)).toBe(false) + expect(Object.isSealed(JOB_DETAIL_CODE)).toBe(false) + }) + }) +}) diff --git a/src/context/__tests__/ApiContext-test.tsx b/src/context/__tests__/ApiContext-test.tsx new file mode 100644 index 0000000000..b2ca6ced5e --- /dev/null +++ b/src/context/__tests__/ApiContext-test.tsx @@ -0,0 +1,382 @@ +import React from 'react' +import { render as rtlRender, screen } from '@testing-library/react' +import userEvent from '@testing-library/user-event' +import { ApiProvider, useApi, defaultApiValue, type ApiContextTypes } from '../ApiContext' + +const TestComponent: React.FC = () => { + const { api } = useApi() + return ( +
+ + +
API Available
+
+ ) +} + +describe('ApiContext', () => { + describe('ApiProvider', () => { + it('should render children', () => { + rtlRender( + +
Test Child
+
, + ) + + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should provide default API values', () => { + rtlRender( + + + , + ) + + expect(screen.getByTestId('api-available')).toBeInTheDocument() + }) + + it('should merge custom API values with defaults', async () => { + const user = userEvent.setup() + const customLoadMembers = vi.fn(() => Promise.resolve([])) + const customApiValue = { + loadMembers: customLoadMembers, + } + + const { getByText } = rtlRender( + + + , + ) + + await user.click(getByText('Load Members')) + + expect(customLoadMembers).toHaveBeenCalled() + }) + + it('should allow custom API values to override defaults', async () => { + const user = userEvent.setup() + const customLoadInstitution = vi.fn(() => + Promise.resolve({ guid: 'INS-123', name: 'Test Bank' } as InstitutionResponseType), + ) + + const { getByText } = rtlRender( + + + , + ) + + await user.click(getByText('Load Institution')) + + expect(customLoadInstitution).toHaveBeenCalledWith('INS-123') + }) + }) + + describe('useApi hook', () => { + it('should return api object when used within ApiProvider', () => { + const TestComponentCheckApi = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') + }) + + it('should return default API values even when used outside provider', () => { + const TestComponentCheckApi = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender() + + expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') + }) + + it('should have all default API methods available', () => { + const TestComponentCheckMethods = () => { + const { api } = useApi() + return ( +
+
+ {typeof api.addMember === 'function' ? 'yes' : 'no'} +
+
+ {typeof api.loadMembers === 'function' ? 'yes' : 'no'} +
+
+ {typeof api.loadInstitutions === 'function' ? 'yes' : 'no'} +
+
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('has-addMember')).toHaveTextContent('yes') + expect(screen.getByTestId('has-loadMembers')).toHaveTextContent('yes') + expect(screen.getByTestId('has-loadInstitutions')).toHaveTextContent('yes') + }) + }) + + describe('defaultApiValue', () => { + it('should have createAccount function', async () => { + expect(defaultApiValue.createAccount).toBeDefined() + const result = await defaultApiValue.createAccount!({} as AccountCreateType) + expect(result).toBeDefined() + }) + + it('should have addMember function', async () => { + expect(defaultApiValue.addMember).toBeDefined() + const result = await defaultApiValue.addMember({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have deleteMember function', async () => { + expect(defaultApiValue.deleteMember).toBeDefined() + await expect(defaultApiValue.deleteMember({} as MemberDeleteType)).resolves.toBeUndefined() + }) + + it('should have getMemberCredentials function', async () => { + expect(defaultApiValue.getMemberCredentials).toBeDefined() + const result = await defaultApiValue.getMemberCredentials('MEM-123') + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadMemberByGuid function', async () => { + expect(defaultApiValue.loadMemberByGuid).toBeDefined() + const result = await defaultApiValue.loadMemberByGuid!('MEM-123') + expect(result).toBeDefined() + }) + + it('should have loadMembers function', async () => { + expect(defaultApiValue.loadMembers).toBeDefined() + const result = await defaultApiValue.loadMembers() + expect(Array.isArray(result)).toBe(true) + }) + + it('should have updateMember function', async () => { + expect(defaultApiValue.updateMember).toBeDefined() + const result = await defaultApiValue.updateMember({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have getInstitutionCredentials function', async () => { + expect(defaultApiValue.getInstitutionCredentials).toBeDefined() + const result = await defaultApiValue.getInstitutionCredentials('INS-123') + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadDiscoveredInstitutions function', async () => { + expect(defaultApiValue.loadDiscoveredInstitutions).toBeDefined() + const result = await defaultApiValue.loadDiscoveredInstitutions!({ + iso_country_code: 'US', + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadInstitutionByCode function', async () => { + expect(defaultApiValue.loadInstitutionByCode).toBeDefined() + const result = await defaultApiValue.loadInstitutionByCode!('mxbank') + expect(result).toBeDefined() + }) + + it('should have loadInstitutions function', async () => { + expect(defaultApiValue.loadInstitutions).toBeDefined() + const result = await defaultApiValue.loadInstitutions({ + routing_number: '123456789', + account_verification_is_enabled: true, + account_identification_is_enabled: false, + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have loadInstitutionByGuid function', async () => { + expect(defaultApiValue.loadInstitutionByGuid).toBeDefined() + const result = await defaultApiValue.loadInstitutionByGuid('INS-123') + expect(result).toBeDefined() + }) + + it('should have loadPopularInstitutions function', async () => { + expect(defaultApiValue.loadPopularInstitutions).toBeDefined() + const result = await defaultApiValue.loadPopularInstitutions({}) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have createMicrodeposit function', async () => { + expect(defaultApiValue.createMicrodeposit).toBeDefined() + const result = await defaultApiValue.createMicrodeposit!({} as MicrodepositCreateType) + expect(result).toBeDefined() + }) + + it('should have loadMicrodepositByGuid function', async () => { + expect(defaultApiValue.loadMicrodepositByGuid).toBeDefined() + const result = await defaultApiValue.loadMicrodepositByGuid!('MICRO-123') + expect(result).toBeDefined() + }) + + it('should have refreshMicrodepositStatus function', async () => { + expect(defaultApiValue.refreshMicrodepositStatus).toBeDefined() + await expect(defaultApiValue.refreshMicrodepositStatus!('MICRO-123')).resolves.toBeUndefined() + }) + + it('should have updateMicrodeposit function', async () => { + expect(defaultApiValue.updateMicrodeposit).toBeDefined() + const result = await defaultApiValue.updateMicrodeposit!( + 'MICRO-123', + {} as MicrodepositUpdateType, + ) + expect(result).toBeDefined() + }) + + it('should have verifyMicrodeposit function', async () => { + expect(defaultApiValue.verifyMicrodeposit).toBeDefined() + const result = await defaultApiValue.verifyMicrodeposit!( + 'MICRO-123', + {} as MicroDepositVerifyType, + ) + expect(result).toBeDefined() + }) + + it('should have verifyRoutingNumber function', async () => { + expect(defaultApiValue.verifyRoutingNumber).toBeDefined() + const result = await defaultApiValue.verifyRoutingNumber!('123456789', true) + expect(result).toBeDefined() + }) + + it('should have updateMFA function', async () => { + expect(defaultApiValue.updateMFA).toBeDefined() + const result = await defaultApiValue.updateMFA({}, {} as ClientConfigType, true) + expect(result).toBeDefined() + }) + + it('should have loadOAuthState function', async () => { + expect(defaultApiValue.loadOAuthState).toBeDefined() + const result = await defaultApiValue.loadOAuthState('OAUTH-123') + expect(result).toBeDefined() + }) + + it('should have loadOAuthStates function', async () => { + expect(defaultApiValue.loadOAuthStates).toBeDefined() + const result = await defaultApiValue.loadOAuthStates({ + outbound_member_guid: 'MEM-123', + auth_status: 'pending', + }) + expect(Array.isArray(result)).toBe(true) + }) + + it('should have oAuthStart function', async () => { + expect(defaultApiValue.oAuthStart).toBeDefined() + await expect(defaultApiValue.oAuthStart!({ member: {} })).resolves.toBeUndefined() + }) + + it('should have createSupportTicket function', async () => { + expect(defaultApiValue.createSupportTicket).toBeDefined() + await expect( + defaultApiValue.createSupportTicket!({} as SupportTicketType), + ).resolves.toBeUndefined() + }) + + it('should have loadJob function', async () => { + expect(defaultApiValue.loadJob).toBeDefined() + const result = await defaultApiValue.loadJob('JOB-123') + expect(result).toBeDefined() + }) + + it('should have runJob function', async () => { + expect(defaultApiValue.runJob).toBeDefined() + const result = await defaultApiValue.runJob( + 'aggregate', + 'MEM-123', + {} as ClientConfigType, + true, + ) + expect(result).toBeDefined() + }) + + it('should have updateUserProfile function', async () => { + expect(defaultApiValue.updateUserProfile).toBeDefined() + const result = await defaultApiValue.updateUserProfile!({ + userProfile: {}, + too_small_modal_dismissed_at: '2024-01-01', + }) + expect(result).toBeDefined() + }) + }) + + describe('Integration tests', () => { + it('should allow calling API methods from components', async () => { + const user = userEvent.setup() + const mockLoadMembers = vi.fn(() => + Promise.resolve([ + { guid: 'MEM-1', name: 'Member 1' }, + { guid: 'MEM-2', name: 'Member 2' }, + ] as MemberResponseType[]), + ) + + const TestComponentWithApi = () => { + const { api } = useApi() + const [members, setMembers] = React.useState([]) + + const handleLoad = async () => { + const result = await api.loadMembers() + setMembers(result) + } + + return ( +
+ +
{members.length}
+
+ ) + } + + const { getByText, getByTestId } = rtlRender( + + + , + ) + + expect(getByTestId('member-count')).toHaveTextContent('0') + + await user.click(getByText('Load')) + + expect(mockLoadMembers).toHaveBeenCalled() + expect(getByTestId('member-count')).toHaveTextContent('2') + }) + + it('should allow multiple components to access the same API context', () => { + const Component1 = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + const Component2 = () => { + const { api } = useApi() + return
{api ? 'Has API' : 'No API'}
+ } + + rtlRender( + + + + , + ) + + expect(screen.getByTestId('comp1')).toHaveTextContent('Has API') + expect(screen.getByTestId('comp2')).toHaveTextContent('Has API') + }) + }) +}) diff --git a/src/context/__tests__/WebSocketContext-test.tsx b/src/context/__tests__/WebSocketContext-test.tsx new file mode 100644 index 0000000000..6c2d2e0a81 --- /dev/null +++ b/src/context/__tests__/WebSocketContext-test.tsx @@ -0,0 +1,208 @@ +import React from 'react' +import { render as rtlRender, screen } from '@testing-library/react' +import { of, Subject } from 'rxjs' +import { WebSocketProvider, useWebSocket, WebSocketConnection } from '../WebSocketContext' + +describe('WebSocketContext', () => { + describe('WebSocketProvider', () => { + it('should render children', () => { + rtlRender( + +
Test Child
+
, + ) + + expect(screen.getByText('Test Child')).toBeInTheDocument() + }) + + it('should provide undefined value when no value prop is passed', () => { + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket === undefined ? 'undefined' : 'defined'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('ws-value')).toHaveTextContent('undefined') + }) + + it('should provide WebSocket connection when value prop is passed', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + return ( +
+ {webSocket?.isConnected() ? 'connected' : 'disconnected'} +
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('ws-connected')).toHaveTextContent('connected') + }) + }) + + describe('useWebSocket hook', () => { + it('should return undefined when used without provider value', () => { + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket ? 'has value' : 'no value'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('result')).toHaveTextContent('no value') + }) + + it('should return WebSocket connection when provided', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({ type: 'message' }), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + return
{webSocket ? 'has value' : 'no value'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('result')).toHaveTextContent('has value') + }) + + it('should allow accessing isConnected method', () => { + const mockConnection: WebSocketConnection = { + isConnected: vi.fn(() => true), + webSocketMessages$: of({}), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + const connected = webSocket?.isConnected() + return
{connected ? 'connected' : 'disconnected'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('status')).toHaveTextContent('connected') + expect(mockConnection.isConnected).toHaveBeenCalled() + }) + + it('should allow subscribing to webSocketMessages$', async () => { + const messageSubject = new Subject() + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: messageSubject.asObservable(), + } + + const TestComponent = () => { + const webSocket = useWebSocket() + const [message, setMessage] = React.useState('') + + React.useEffect(() => { + if (webSocket) { + const subscription = webSocket.webSocketMessages$.subscribe((msg: { text: string }) => { + setMessage(msg.text) + }) + return () => subscription.unsubscribe() + } + return undefined + }, [webSocket]) + + return
{message || 'no message'}
+ } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('message')).toHaveTextContent('no message') + + messageSubject.next({ text: 'Hello WebSocket' }) + + await new Promise((resolve) => setTimeout(resolve, 0)) + + expect(screen.getByTestId('message')).toHaveTextContent('Hello WebSocket') + }) + }) + + describe('Integration tests', () => { + it('should allow multiple components to access the same WebSocket connection', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } + + const Component1 = () => { + const ws = useWebSocket() + return
{ws?.isConnected() ? 'connected' : 'disconnected'}
+ } + + const Component2 = () => { + const ws = useWebSocket() + return
{ws?.isConnected() ? 'connected' : 'disconnected'}
+ } + + rtlRender( + + + + , + ) + + expect(screen.getByTestId('comp1')).toHaveTextContent('connected') + expect(screen.getByTestId('comp2')).toHaveTextContent('connected') + }) + + it('should handle disconnected state', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({}), + } + + const TestComponent = () => { + const ws = useWebSocket() + return ( +
+ {ws?.isConnected() ? 'Connected' : 'Disconnected'} +
+ ) + } + + rtlRender( + + + , + ) + + expect(screen.getByTestId('connection-status')).toHaveTextContent('Disconnected') + }) + }) +}) diff --git a/src/privacy/__tests__/withProtection-test.tsx b/src/privacy/__tests__/withProtection-test.tsx new file mode 100644 index 0000000000..9a72b5da27 --- /dev/null +++ b/src/privacy/__tests__/withProtection-test.tsx @@ -0,0 +1,230 @@ +import React from 'react' +import { screen } from '@testing-library/react' +import { describe, it, expect } from 'vitest' +import { maskInputFn, withProtection } from '../withProtection' +import { render } from '../../utilities/testingLibrary' + +describe('maskInputFn', () => { + it('should mask input text with asterisks by default', () => { + const result = maskInputFn('password123') + expect(result).toBe('***********') + }) + + it('should mask input text when no element is provided', () => { + const result = maskInputFn('secretText') + expect(result).toBe('**********') + }) + + it('should mask input text when element does not have unmask attribute', () => { + const element = document.createElement('input') + const result = maskInputFn('myPassword', element) + 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') + }) + + it('should mask text when element has data-ph-unmask="false"', () => { + const element = document.createElement('input') + element.setAttribute('data-ph-unmask', 'false') + const result = maskInputFn('secretData', element) + expect(result).toBe('**********') + }) + + it('should mask empty string', () => { + const result = maskInputFn('') + expect(result).toBe('') + }) + + it('should mask single character', () => { + const result = maskInputFn('x') + expect(result).toBe('*') + }) +}) + +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') + }) + + it('should not add data-ph-unmask attribute when allowCapture is false', () => { + const TestComponent = React.forwardRef< + HTMLInputElement, + { 'data-test': string; 'data-ph-unmask'?: boolean } + >((props, ref) => ) + TestComponent.displayName = 'TestComponent' + + const ProtectedComponent = withProtection(TestComponent) + + render() + + const wrapper = document.querySelector('.ph-no-capture') + expect(wrapper).toBeTruthy() + + const input = screen.getByTestId('test-input') + expect(input.hasAttribute('data-ph-unmask')).toBe(false) + }) + + it('should pass through other props correctly', () => { + const TestComponent = ({ + 'data-test': dataTest, + className, + id, + }: { + 'data-test': string + className?: string + id?: string + }) => ( +
+ Content +
+ ) + const ProtectedComponent = withProtection(TestComponent) + + render( + , + ) + + const element = screen.getByTestId('test-component') + expect(element).toHaveClass('custom-class') + expect(element).toHaveAttribute('id', 'custom-id') + }) + + it('should forward ref correctly', () => { + const TestComponent = React.forwardRef< + HTMLButtonElement, + { 'data-test': string; children: React.ReactNode } + >((props, ref) => - -
, - { preloadedState }, - ) - - expect(screen.getByTestId('test-form')).toBeInTheDocument() - expect(screen.getByTestId('test-input')).toBeInTheDocument() - expect(screen.getByTestId('test-button')).toBeInTheDocument() - }) - - it('wraps components consistently regardless of content type', () => { - const { container } = render( - -
- Text - test - -
-
, - { preloadedState }, - ) + it('applies maxHeight when step is SEARCH', () => { + const { container } = render( + +
Content
+
, + { preloadedState }, + ) - const containerDiv = container.querySelector('[data-test="container"]') - expect(containerDiv).toBeInTheDocument() - expect(screen.getByText('Text')).toBeInTheDocument() - expect(screen.getByAltText('test')).toBeInTheDocument() - expect(screen.getByRole('button', { name: 'Click' })).toBeInTheDocument() - }) + const containerDiv = container.querySelector('[data-test="container"]') + expect(containerDiv).toHaveStyle({ maxHeight: '100%' }) }) }) From 2e67c7d28c34d0199550312e8eb7e179925058d6 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Tue, 23 Jun 2026 16:34:10 -0600 Subject: [PATCH 10/18] refactored delete member survey --- src/Connect.tsx | 32 +- src/components/DeleteMemberSurvey-test.tsx | 515 +++++++++------------ src/components/DeleteMemberSurvey.js | 22 +- 3 files changed, 236 insertions(+), 333 deletions(-) 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/DeleteMemberSurvey-test.tsx b/src/components/DeleteMemberSurvey-test.tsx index 3e44e1c844..efab4fe9e4 100644 --- a/src/components/DeleteMemberSurvey-test.tsx +++ b/src/components/DeleteMemberSurvey-test.tsx @@ -1,345 +1,248 @@ import React from 'react' -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi } from 'vitest' import { render, screen, waitFor } from 'src/utilities/testingLibrary' import { DeleteMemberSurvey } from 'src/components/DeleteMemberSurvey' -import { initialState, CONNECTED_MEMBER, NON_CONNECTED_MEMBER } from 'src/services/mockedData' +import { initialState, CONNECTED_MEMBER } from 'src/services/mockedData' import userEvent from '@testing-library/user-event' +import { apiValue as mockApiValue } from 'src/const/apiProviderMock' describe('DeleteMemberSurvey', () => { const preloadedState = initialState - const mockOnCancel = vi.fn() - const mockOnDeleteSuccess = vi.fn() - - beforeEach(() => { - vi.clearAllMocks() + it('does not render when isOpen is false', () => { + const { container } = render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(container.firstChild).toBeNull() }) - describe('rendering', () => { - it('renders the disconnect institution dialog', () => { - const { container } = render( - , - { preloadedState }, - ) - - const dialog = container.querySelector('[role="dialog"]') - expect(dialog).toBeInTheDocument() - }) - - it('renders the disconnect institution heading', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - }) + 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('renders the disclaimer with member name', () => { - render( - , - { preloadedState }, - ) - - const disclaimer = screen.getByTestId('disconnect-disclaimer') - expect(disclaimer).toBeInTheDocument() - expect(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('renders disconnect and cancel buttons', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() - }) + it('shows connected member reasons', () => { + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() + expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() + expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() + }) - it('renders required field indicator', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('Required')).toBeInTheDocument() - }) + it('shows non-connected member reasons', () => { + const nonConnectedMember = { ...CONNECTED_MEMBER, connection_status: 1 } + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) + + expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() + expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() + expect( + screen.queryByText("I no longer use this account or it's not mine"), + ).not.toBeInTheDocument() }) - describe('connected member reasons', () => { - it('renders correct reasons for connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText("I no longer use this account or it's not mine")).toBeInTheDocument() - expect(screen.getByText("I don't want to share my data")).toBeInTheDocument() - expect(screen.getByText("I don't want to use this app")).toBeInTheDocument() - expect(screen.getByText('Other')).toBeInTheDocument() - }) + it('shows validation error when no reason selected', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) - it('does not render non-connected reasons for connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.queryByText('I am unable to connect this account here')).not.toBeInTheDocument() - expect( - screen.queryByText('The account information is old or inaccurate'), - ).not.toBeInTheDocument() - expect(screen.queryByText("I don't want this account connected here")).not.toBeInTheDocument() - }) - }) + await user.click(screen.getByTestId('disconnect-button')) - describe('non-connected member reasons', () => { - it('renders correct reasons for non-connected member', () => { - render( - , - { preloadedState }, - ) - - expect(screen.getByText('I am unable to connect this account here')).toBeInTheDocument() - expect(screen.getByText('The account information is old or inaccurate')).toBeInTheDocument() - expect(screen.getByText("I don't want this account connected here")).toBeInTheDocument() - expect(screen.getByText('Other')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() }) + }) - it('does not render connected-only reasons for non-connected member', () => { - render( - , - { preloadedState }, - ) - - expect( - screen.queryByText("I no longer use this account or it's not mine"), - ).not.toBeInTheDocument() - expect(screen.queryByText("I don't want to share my data")).not.toBeInTheDocument() - expect(screen.queryByText("I don't want to use this app")).not.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() }) - describe('user interactions', () => { - it('calls onCancel when cancel button is clicked', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - await user.click(screen.getByTestId('disconnect-cancel-button')) - - expect(mockOnCancel).toHaveBeenCalledTimes(1) - }) + it('clears validation error after selecting a reason', async () => { + const user = userEvent.setup() + render( + {}} + onMemberDeleted={() => {}} + />, + { preloadedState }, + ) - it('allows selecting a reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - - expect(options[0]).toBeChecked() - }) + await user.click(screen.getByTestId('disconnect-button')) - it('allows changing selected reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - expect(options[0]).toBeChecked() - - await user.click(options[1]) - expect(options[1]).toBeChecked() - expect(options[0]).not.toBeChecked() + await waitFor(() => { + expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() }) - }) - describe('form validation', () => { - it('shows validation error when disconnect clicked without selecting reason', async () => { - const user = userEvent.setup() - render( - , - { 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) - it('does not show validation error before first submit attempt', () => { - render( - , - { preloadedState }, - ) - - expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() - }) + expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() + }) - it('validation error disappears after selecting a reason', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - await user.click(screen.getByTestId('disconnect-button')) - - await waitFor(() => { - expect(screen.getByText('Choose a reason for deleting')).toBeInTheDocument() - }) - const options = screen.getAllByRole('radio') - await user.click(options[0]) - - await waitFor(() => { - 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() }) }) - describe('delete member flow', () => { - it('initiates delete when disconnect clicked with valid selection', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) + 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() + }) - const options = screen.getAllByRole('radio') - await user.click(options[0]) + 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'))), + } - await user.click(screen.getByTestId('disconnect-button')) + render( + {}} + />, + { apiValue, preloadedState }, + ) - expect(screen.queryByText('Choose a reason for deleting')).not.toBeInTheDocument() - }) - }) + const firstReason = screen.getAllByRole('radio')[0] + await user.click(firstReason) + await user.click(screen.getByTestId('disconnect-button')) - describe('integration', () => { - it('renders complete structure for connected member', () => { - const { container } = render( - , - { preloadedState }, - ) - - expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-disclaimer')).toBeInTheDocument() - expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) - expect(screen.getByTestId('disconnect-button')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-cancel-button')).toBeInTheDocument() + await waitFor(() => { + expect(screen.getByTestId('disconnect-error-header')).toBeInTheDocument() }) - it('renders complete structure for non-connected member', () => { - const { container } = render( - , - { preloadedState }, - ) - - expect(container.querySelector('[role="dialog"]')).toBeInTheDocument() - expect(screen.getByText('Disconnect institution')).toBeInTheDocument() - expect(screen.getByTestId('disconnect-disclaimer').textContent).toContain('Wells Fargo') - expect(screen.getAllByRole('radio').length).toBeGreaterThan(0) - }) + await user.click(screen.getByTestId('disconnect-ok-button')) - it('handles complete user flow from selection to cancel', async () => { - const user = userEvent.setup() - render( - , - { preloadedState }, - ) - - const options = screen.getAllByRole('radio') - await user.click(options[0]) - expect(options[0]).toBeChecked() - - await user.click(screen.getByTestId('disconnect-cancel-button')) - expect(mockOnCancel).toHaveBeenCalledTimes(1) - }) + expect(onClose).toHaveBeenCalled() }) }) diff --git a/src/components/DeleteMemberSurvey.js b/src/components/DeleteMemberSurvey.js index 9d8565541f..350a9c5114 100644 --- a/src/components/DeleteMemberSurvey.js +++ b/src/components/DeleteMemberSurvey.js @@ -19,7 +19,7 @@ import { PageviewInfo } from 'src/const/Analytics' import { ReadableStatuses } from 'src/const/Statuses' 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() @@ -59,12 +59,17 @@ export const DeleteMemberSurvey = (props) => { 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) => {
- -
API Available
-
- ) -} +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', () => { - describe('ApiProvider', () => { - it('should render children', () => { - rtlRender( - -
Test Child
-
, - ) - - expect(screen.getByText('Test Child')).toBeInTheDocument() - }) - - it('should provide default API values', () => { - rtlRender( - - - , - ) - - expect(screen.getByTestId('api-available')).toBeInTheDocument() - }) - - it('should merge custom API values with defaults', async () => { - const user = userEvent.setup() - const customLoadMembers = vi.fn(() => Promise.resolve([])) - const customApiValue = { - loadMembers: customLoadMembers, - } - - const { getByText } = rtlRender( - - - , - ) - - await user.click(getByText('Load Members')) - - expect(customLoadMembers).toHaveBeenCalled() - }) - - it('should allow custom API values to override defaults', async () => { - const user = userEvent.setup() - const customLoadInstitution = vi.fn(() => - Promise.resolve({ guid: 'INS-123', name: 'Test Bank' } as InstitutionResponseType), - ) - - const { getByText } = rtlRender( - - - , - ) - - await user.click(getByText('Load Institution')) - - expect(customLoadInstitution).toHaveBeenCalledWith('INS-123') - }) + 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() }) - describe('useApi hook', () => { - it('should return api object when used within ApiProvider', () => { - const TestComponentCheckApi = () => { - const { api } = useApi() - return
{api ? 'Has API' : 'No API'}
- } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') - }) - - it('should return default API values even when used outside provider', () => { - const TestComponentCheckApi = () => { - const { api } = useApi() - return
{api ? 'Has API' : 'No API'}
- } - - rtlRender() - - expect(screen.getByTestId('has-api')).toHaveTextContent('Has API') - }) - - it('should have all default API methods available', () => { - const TestComponentCheckMethods = () => { - const { api } = useApi() - return ( -
-
- {typeof api.addMember === 'function' ? 'yes' : 'no'} -
-
- {typeof api.loadMembers === 'function' ? 'yes' : 'no'} -
-
- {typeof api.loadInstitutions === 'function' ? 'yes' : 'no'} -
-
- ) - } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('has-addMember')).toHaveTextContent('yes') - expect(screen.getByTestId('has-loadMembers')).toHaveTextContent('yes') - expect(screen.getByTestId('has-loadInstitutions')).toHaveTextContent('yes') - }) + 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() }) - describe('defaultApiValue', () => { - it('should have createAccount function', async () => { - expect(defaultApiValue.createAccount).toBeDefined() - const result = await defaultApiValue.createAccount!({} as AccountCreateType) - expect(result).toBeDefined() - }) - - it('should have addMember function', async () => { - expect(defaultApiValue.addMember).toBeDefined() - const result = await defaultApiValue.addMember({}, {} as ClientConfigType, true) - expect(result).toBeDefined() - }) - - it('should have deleteMember function', async () => { - expect(defaultApiValue.deleteMember).toBeDefined() - await expect(defaultApiValue.deleteMember({} as MemberDeleteType)).resolves.toBeUndefined() - }) - - it('should have getMemberCredentials function', async () => { - expect(defaultApiValue.getMemberCredentials).toBeDefined() - const result = await defaultApiValue.getMemberCredentials('MEM-123') - expect(Array.isArray(result)).toBe(true) - }) - - it('should have loadMemberByGuid function', async () => { - expect(defaultApiValue.loadMemberByGuid).toBeDefined() - const result = await defaultApiValue.loadMemberByGuid!('MEM-123') - expect(result).toBeDefined() - }) - - it('should have loadMembers function', async () => { - expect(defaultApiValue.loadMembers).toBeDefined() - const result = await defaultApiValue.loadMembers() - expect(Array.isArray(result)).toBe(true) - }) - - it('should have updateMember function', async () => { - expect(defaultApiValue.updateMember).toBeDefined() - const result = await defaultApiValue.updateMember({}, {} as ClientConfigType, true) - expect(result).toBeDefined() - }) - - it('should have getInstitutionCredentials function', async () => { - expect(defaultApiValue.getInstitutionCredentials).toBeDefined() - const result = await defaultApiValue.getInstitutionCredentials('INS-123') - expect(Array.isArray(result)).toBe(true) - }) - - it('should have loadDiscoveredInstitutions function', async () => { - expect(defaultApiValue.loadDiscoveredInstitutions).toBeDefined() - const result = await defaultApiValue.loadDiscoveredInstitutions!({ - iso_country_code: 'US', - }) - expect(Array.isArray(result)).toBe(true) - }) - - it('should have loadInstitutionByCode function', async () => { - expect(defaultApiValue.loadInstitutionByCode).toBeDefined() - const result = await defaultApiValue.loadInstitutionByCode!('mxbank') - expect(result).toBeDefined() - }) - - it('should have loadInstitutions function', async () => { - expect(defaultApiValue.loadInstitutions).toBeDefined() - const result = await defaultApiValue.loadInstitutions({ - routing_number: '123456789', - account_verification_is_enabled: true, - account_identification_is_enabled: false, - }) - expect(Array.isArray(result)).toBe(true) - }) - - it('should have loadInstitutionByGuid function', async () => { - expect(defaultApiValue.loadInstitutionByGuid).toBeDefined() - const result = await defaultApiValue.loadInstitutionByGuid('INS-123') - expect(result).toBeDefined() - }) - - it('should have loadPopularInstitutions function', async () => { - expect(defaultApiValue.loadPopularInstitutions).toBeDefined() - const result = await defaultApiValue.loadPopularInstitutions({}) - expect(Array.isArray(result)).toBe(true) - }) - - it('should have createMicrodeposit function', async () => { - expect(defaultApiValue.createMicrodeposit).toBeDefined() - const result = await defaultApiValue.createMicrodeposit!({} as MicrodepositCreateType) - expect(result).toBeDefined() - }) - - it('should have loadMicrodepositByGuid function', async () => { - expect(defaultApiValue.loadMicrodepositByGuid).toBeDefined() - const result = await defaultApiValue.loadMicrodepositByGuid!('MICRO-123') - expect(result).toBeDefined() - }) - - it('should have refreshMicrodepositStatus function', async () => { - expect(defaultApiValue.refreshMicrodepositStatus).toBeDefined() - await expect(defaultApiValue.refreshMicrodepositStatus!('MICRO-123')).resolves.toBeUndefined() - }) - - it('should have updateMicrodeposit function', async () => { - expect(defaultApiValue.updateMicrodeposit).toBeDefined() - const result = await defaultApiValue.updateMicrodeposit!( - 'MICRO-123', - {} as MicrodepositUpdateType, - ) - expect(result).toBeDefined() - }) - - it('should have verifyMicrodeposit function', async () => { - expect(defaultApiValue.verifyMicrodeposit).toBeDefined() - const result = await defaultApiValue.verifyMicrodeposit!( - 'MICRO-123', - {} as MicroDepositVerifyType, - ) - expect(result).toBeDefined() - }) - - it('should have verifyRoutingNumber function', async () => { - expect(defaultApiValue.verifyRoutingNumber).toBeDefined() - const result = await defaultApiValue.verifyRoutingNumber!('123456789', true) - expect(result).toBeDefined() - }) - - it('should have updateMFA function', async () => { - expect(defaultApiValue.updateMFA).toBeDefined() - const result = await defaultApiValue.updateMFA({}, {} as ClientConfigType, true) - expect(result).toBeDefined() - }) - - it('should have loadOAuthState function', async () => { - expect(defaultApiValue.loadOAuthState).toBeDefined() - const result = await defaultApiValue.loadOAuthState('OAUTH-123') - expect(result).toBeDefined() - }) - - it('should have loadOAuthStates function', async () => { - expect(defaultApiValue.loadOAuthStates).toBeDefined() - const result = await defaultApiValue.loadOAuthStates({ - outbound_member_guid: 'MEM-123', - auth_status: 'pending', - }) - expect(Array.isArray(result)).toBe(true) - }) - - it('should have oAuthStart function', async () => { - expect(defaultApiValue.oAuthStart).toBeDefined() - await expect(defaultApiValue.oAuthStart!({ member: {} })).resolves.toBeUndefined() - }) - - it('should have createSupportTicket function', async () => { - expect(defaultApiValue.createSupportTicket).toBeDefined() - await expect( - defaultApiValue.createSupportTicket!({} as SupportTicketType), - ).resolves.toBeUndefined() - }) - - it('should have loadJob function', async () => { - expect(defaultApiValue.loadJob).toBeDefined() - const result = await defaultApiValue.loadJob('JOB-123') - expect(result).toBeDefined() - }) - - it('should have runJob function', async () => { - expect(defaultApiValue.runJob).toBeDefined() - const result = await defaultApiValue.runJob( - 'aggregate', - 'MEM-123', - {} as ClientConfigType, - true, - ) - expect(result).toBeDefined() - }) - - it('should have updateUserProfile function', async () => { - expect(defaultApiValue.updateUserProfile).toBeDefined() - const result = await defaultApiValue.updateUserProfile!({ - userProfile: {}, - too_small_modal_dismissed_at: '2024-01-01', - }) - expect(result).toBeDefined() - }) - }) - - describe('Integration tests', () => { - it('should allow calling API methods from components', async () => { - const user = userEvent.setup() - const mockLoadMembers = vi.fn(() => - Promise.resolve([ - { guid: 'MEM-1', name: 'Member 1' }, - { guid: 'MEM-2', name: 'Member 2' }, - ] as MemberResponseType[]), + it('provides default API values when used outside provider', () => { + const TestComponent = () => { + const { api } = useApi() + return ( +
+
{typeof api.loadMembers === 'function' ? 'yes' : 'no'}
+
) + } - const TestComponentWithApi = () => { - const { api } = useApi() - const [members, setMembers] = React.useState([]) + render(, { preloadedState }) - const handleLoad = async () => { - const result = await api.loadMembers() - setMembers(result) - } - - return ( -
- -
{members.length}
-
- ) - } - - const { getByText, getByTestId } = rtlRender( - - - , - ) - - expect(getByTestId('member-count')).toHaveTextContent('0') - - await user.click(getByText('Load')) - - expect(mockLoadMembers).toHaveBeenCalled() - expect(getByTestId('member-count')).toHaveTextContent('2') - }) - - it('should allow multiple components to access the same API context', () => { - const Component1 = () => { - const { api } = useApi() - return
{api ? 'Has API' : 'No API'}
- } - - const Component2 = () => { - const { api } = useApi() - return
{api ? 'Has API' : 'No API'}
- } - - rtlRender( - - - - , - ) - - expect(screen.getByTestId('comp1')).toHaveTextContent('Has API') - expect(screen.getByTestId('comp2')).toHaveTextContent('Has API') - }) + 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 index cb9bc2fefe..79c791d5fc 100644 --- a/src/context/WebSocketContext-test.tsx +++ b/src/context/WebSocketContext-test.tsx @@ -1,208 +1,74 @@ import React from 'react' -import { render as rtlRender, screen } from '@testing-library/react' -import { of, Subject } from 'rxjs' +import { renderHook } from '@testing-library/react' +import { of } from 'rxjs' import { WebSocketProvider, useWebSocket, WebSocketConnection } from 'src/context/WebSocketContext' describe('WebSocketContext', () => { - describe('WebSocketProvider', () => { - it('should render children', () => { - rtlRender( - -
Test Child
-
, - ) - - expect(screen.getByText('Test Child')).toBeInTheDocument() - }) - - it('should provide undefined value when no value prop is passed', () => { - const TestComponent = () => { - const webSocket = useWebSocket() - return
{webSocket === undefined ? 'undefined' : 'defined'}
- } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('ws-value')).toHaveTextContent('undefined') + it('should return undefined when no WebSocket connection is provided', () => { + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => {children}, }) - it('should provide WebSocket connection when value prop is passed', () => { - const mockConnection: WebSocketConnection = { - isConnected: () => true, - webSocketMessages$: of({ type: 'test' }), - } - - const TestComponent = () => { - const webSocket = useWebSocket() - return ( -
- {webSocket?.isConnected() ? 'connected' : 'disconnected'} -
- ) - } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('ws-connected')).toHaveTextContent('connected') - }) + expect(result.current).toBeUndefined() }) - describe('useWebSocket hook', () => { - it('should return undefined when used without provider value', () => { - const TestComponent = () => { - const webSocket = useWebSocket() - return
{webSocket ? 'has value' : 'no value'}
- } - - rtlRender( - - - , - ) + it('should return the WebSocket connection when provided', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => true, + webSocketMessages$: of({ type: 'test' }), + } - expect(screen.getByTestId('result')).toHaveTextContent('no value') + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - it('should return WebSocket connection when provided', () => { - const mockConnection: WebSocketConnection = { - isConnected: () => false, - webSocketMessages$: of({ type: 'message' }), - } - - const TestComponent = () => { - const webSocket = useWebSocket() - return
{webSocket ? 'has value' : 'no value'}
- } + expect(result.current).toBe(mockConnection) + expect(result.current?.isConnected()).toBe(true) + }) - rtlRender( - - - , - ) + it('should allow accessing webSocketMessages$ observable', () => { + const mockConnection: WebSocketConnection = { + isConnected: () => false, + webSocketMessages$: of({ event: 'test', payload: { id: 123 } }), + } - expect(screen.getByTestId('result')).toHaveTextContent('has value') + const { result } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - it('should allow accessing isConnected method', () => { - const mockConnection: WebSocketConnection = { - isConnected: vi.fn(() => true), - webSocketMessages$: of({}), - } + expect(result.current?.webSocketMessages$).toBeDefined() - const TestComponent = () => { - const webSocket = useWebSocket() - const connected = webSocket?.isConnected() - return
{connected ? 'connected' : 'disconnected'}
- } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('status')).toHaveTextContent('connected') - expect(mockConnection.isConnected).toHaveBeenCalled() + let receivedMessage: unknown + result.current?.webSocketMessages$.subscribe((msg) => { + receivedMessage = msg }) - it('should allow subscribing to webSocketMessages$', async () => { - const messageSubject = new Subject() - const mockConnection: WebSocketConnection = { - isConnected: () => true, - webSocketMessages$: messageSubject.asObservable(), - } - - const TestComponent = () => { - const webSocket = useWebSocket() - const [message, setMessage] = React.useState('') - - React.useEffect(() => { - if (webSocket) { - const subscription = webSocket.webSocketMessages$.subscribe((msg: { text: string }) => { - setMessage(msg.text) - }) - return () => subscription.unsubscribe() - } - return undefined - }, [webSocket]) - - return
{message || 'no message'}
- } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('message')).toHaveTextContent('no message') - - messageSubject.next({ text: 'Hello WebSocket' }) - - await new Promise((resolve) => setTimeout(resolve, 0)) - - expect(screen.getByTestId('message')).toHaveTextContent('Hello WebSocket') - }) + expect(receivedMessage).toEqual({ event: 'test', payload: { id: 123 } }) }) - describe('Integration tests', () => { - it('should allow multiple components to access the same WebSocket connection', () => { - const mockConnection: WebSocketConnection = { - isConnected: () => true, - webSocketMessages$: of({ type: 'test' }), - } + it('should provide the same connection to multiple consumers', () => { + const mockConnection: WebSocketConnection = { + isConnected: vi.fn(() => true), + webSocketMessages$: of({}), + } - const Component1 = () => { - const ws = useWebSocket() - return
{ws?.isConnected() ? 'connected' : 'disconnected'}
- } - - const Component2 = () => { - const ws = useWebSocket() - return
{ws?.isConnected() ? 'connected' : 'disconnected'}
- } - - rtlRender( - - - - , - ) - - expect(screen.getByTestId('comp1')).toHaveTextContent('connected') - expect(screen.getByTestId('comp2')).toHaveTextContent('connected') + const { result: result1 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) - it('should handle disconnected state', () => { - const mockConnection: WebSocketConnection = { - isConnected: () => false, - webSocketMessages$: of({}), - } - - const TestComponent = () => { - const ws = useWebSocket() - return ( -
- {ws?.isConnected() ? 'Connected' : 'Disconnected'} -
- ) - } - - rtlRender( - - - , - ) - - expect(screen.getByTestId('connection-status')).toHaveTextContent('Disconnected') + const { result: result2 } = renderHook(() => useWebSocket(), { + wrapper: ({ children }) => ( + {children} + ), }) + + expect(result1.current).toBe(mockConnection) + expect(result2.current).toBe(mockConnection) }) }) diff --git a/src/privacy/withProtection-test.tsx b/src/privacy/withProtection-test.tsx index a475e06461..9dd2963d46 100644 --- a/src/privacy/withProtection-test.tsx +++ b/src/privacy/withProtection-test.tsx @@ -5,45 +5,17 @@ import { maskInputFn, withProtection } from 'src/privacy/withProtection' import { render } from 'src/utilities/testingLibrary' describe('maskInputFn', () => { - it('should mask input text with asterisks by default', () => { + it('should mask input text by default', () => { const result = maskInputFn('password123') expect(result).toBe('***********') }) - it('should mask input text when no element is provided', () => { - const result = maskInputFn('secretText') - expect(result).toBe('**********') - }) - - it('should mask input text when element does not have unmask attribute', () => { - const element = document.createElement('input') - const result = maskInputFn('myPassword', element) - 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') }) - - it('should mask text when element has data-ph-unmask="false"', () => { - const element = document.createElement('input') - element.setAttribute('data-ph-unmask', 'false') - const result = maskInputFn('secretData', element) - expect(result).toBe('**********') - }) - - it('should mask empty string', () => { - const result = maskInputFn('') - expect(result).toBe('') - }) - - it('should mask single character', () => { - const result = maskInputFn('x') - expect(result).toBe('*') - }) }) describe('withProtection', () => { @@ -87,144 +59,4 @@ describe('withProtection', () => { const input = screen.getByTestId('test-input') expect(input.getAttribute('data-ph-unmask')).toBe('true') }) - - it('should not add data-ph-unmask attribute when allowCapture is false', () => { - const TestComponent = React.forwardRef< - HTMLInputElement, - { 'data-test': string; 'data-ph-unmask'?: boolean } - >((props, ref) => ) - TestComponent.displayName = 'TestComponent' - - const ProtectedComponent = withProtection(TestComponent) - - render() - - const wrapper = document.querySelector('.ph-no-capture') - expect(wrapper).toBeTruthy() - - const input = screen.getByTestId('test-input') - expect(input.hasAttribute('data-ph-unmask')).toBe(false) - }) - - it('should pass through other props correctly', () => { - const TestComponent = ({ - 'data-test': dataTest, - className, - id, - }: { - 'data-test': string - className?: string - id?: string - }) => ( -
- Content -
- ) - const ProtectedComponent = withProtection(TestComponent) - - render( - , - ) - - const element = screen.getByTestId('test-component') - expect(element).toHaveClass('custom-class') - expect(element).toHaveAttribute('id', 'custom-id') - }) - - it('should forward ref correctly', () => { - const TestComponent = React.forwardRef< - HTMLButtonElement, - { 'data-test': string; children: React.ReactNode } - >((props, ref) => -
- - - {__('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, -} From 1a3cef51a536906b46ef86a86e33c6f87d2cbd98 Mon Sep 17 00:00:00 2001 From: Ashley Wright Date: Fri, 26 Jun 2026 13:11:57 -0600 Subject: [PATCH 18/18] removed unneeded tests --- src/const/Accounts-test.tsx | 162 ------------------------------- src/const/jobDetailCode-test.tsx | 51 ---------- 2 files changed, 213 deletions(-) delete mode 100644 src/const/Accounts-test.tsx delete mode 100644 src/const/jobDetailCode-test.tsx diff --git a/src/const/Accounts-test.tsx b/src/const/Accounts-test.tsx deleted file mode 100644 index 5f4d1ffeb9..0000000000 --- a/src/const/Accounts-test.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { AccountTypeNames, ReadableAccountTypes } from 'src/const/Accounts' - -describe('Accounts Constants', () => { - describe('ReadableAccountTypes', () => { - it('should have UNKNOWN as 0', () => { - expect(ReadableAccountTypes.UNKNOWN).toBe(0) - }) - - it('should have CHECKING as 1', () => { - expect(ReadableAccountTypes.CHECKING).toBe(1) - }) - - it('should have SAVINGS as 2', () => { - expect(ReadableAccountTypes.SAVINGS).toBe(2) - }) - - it('should have LOAN as 3', () => { - expect(ReadableAccountTypes.LOAN).toBe(3) - }) - - it('should have CREDIT_CARD as 4', () => { - expect(ReadableAccountTypes.CREDIT_CARD).toBe(4) - }) - - it('should have INVESTMENT as 5', () => { - expect(ReadableAccountTypes.INVESTMENT).toBe(5) - }) - - it('should have LINE_OF_CREDIT as 6', () => { - expect(ReadableAccountTypes.LINE_OF_CREDIT).toBe(6) - }) - - it('should have MORTGAGE as 7', () => { - expect(ReadableAccountTypes.MORTGAGE).toBe(7) - }) - - it('should have PROPERTY as 8', () => { - expect(ReadableAccountTypes.PROPERTY).toBe(8) - }) - - it('should have CASH as 9', () => { - expect(ReadableAccountTypes.CASH).toBe(9) - }) - - it('should have INSURANCE as 10', () => { - expect(ReadableAccountTypes.INSURANCE).toBe(10) - }) - - it('should have PREPAID as 11', () => { - expect(ReadableAccountTypes.PREPAID).toBe(11) - }) - - it('should have CHECKING_LINE_OF_CREDIT as 12', () => { - expect(ReadableAccountTypes.CHECKING_LINE_OF_CREDIT).toBe(12) - }) - - it('should have exactly 13 account types', () => { - expect(Object.keys(ReadableAccountTypes)).toHaveLength(13) - }) - - it('should have all numeric values', () => { - Object.values(ReadableAccountTypes).forEach((value) => { - expect(typeof value).toBe('number') - }) - }) - - it('should have unique values', () => { - const values = Object.values(ReadableAccountTypes) - const uniqueValues = new Set(values) - expect(uniqueValues.size).toBe(values.length) - }) - }) - - describe('AccountTypeNames', () => { - it('should have 13 account type names', () => { - expect(AccountTypeNames).toHaveLength(13) - }) - - it('should have "Other" at index 0 for UNKNOWN', () => { - expect(AccountTypeNames[ReadableAccountTypes.UNKNOWN]).toBe('Other') - }) - - it('should have "Checking" at index 1 for CHECKING', () => { - expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') - }) - - it('should have "Savings" at index 2 for SAVINGS', () => { - expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') - }) - - it('should have "Loan" at index 3 for LOAN', () => { - expect(AccountTypeNames[ReadableAccountTypes.LOAN]).toBe('Loan') - }) - - it('should have "Credit Card" at index 4 for CREDIT_CARD', () => { - expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') - }) - - it('should have "Investment" at index 5 for INVESTMENT', () => { - expect(AccountTypeNames[ReadableAccountTypes.INVESTMENT]).toBe('Investment') - }) - - it('should have "Line of Credit" at index 6 for LINE_OF_CREDIT', () => { - expect(AccountTypeNames[ReadableAccountTypes.LINE_OF_CREDIT]).toBe('Line of Credit') - }) - - it('should have "Mortgage" at index 7 for MORTGAGE', () => { - expect(AccountTypeNames[ReadableAccountTypes.MORTGAGE]).toBe('Mortgage') - }) - - it('should have "Property" at index 8 for PROPERTY', () => { - expect(AccountTypeNames[ReadableAccountTypes.PROPERTY]).toBe('Property') - }) - - it('should have "Cash" at index 9 for CASH', () => { - expect(AccountTypeNames[ReadableAccountTypes.CASH]).toBe('Cash') - }) - - it('should have "Insurance" at index 10 for INSURANCE', () => { - expect(AccountTypeNames[ReadableAccountTypes.INSURANCE]).toBe('Insurance') - }) - - it('should have "Prepaid" at index 11 for PREPAID', () => { - expect(AccountTypeNames[ReadableAccountTypes.PREPAID]).toBe('Prepaid') - }) - - it('should have "Checking" at index 12 for CHECKING_LINE_OF_CREDIT', () => { - expect(AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT]).toBe('Checking') - }) - - it('should have all string values', () => { - AccountTypeNames.forEach((name) => { - expect(typeof name).toBe('string') - }) - }) - }) - - describe('Integration between ReadableAccountTypes and AccountTypeNames', () => { - it('should map all ReadableAccountTypes to valid AccountTypeNames', () => { - Object.entries(ReadableAccountTypes).forEach(([_key, value]) => { - expect(AccountTypeNames[value]).toBeDefined() - expect(typeof AccountTypeNames[value]).toBe('string') - }) - }) - - it('should have correct mapping for UNKNOWN type', () => { - const name = AccountTypeNames[ReadableAccountTypes.UNKNOWN] - expect(name).toBe('Other') - }) - - it('should have correct mapping for standard account types', () => { - expect(AccountTypeNames[ReadableAccountTypes.CHECKING]).toBe('Checking') - expect(AccountTypeNames[ReadableAccountTypes.SAVINGS]).toBe('Savings') - expect(AccountTypeNames[ReadableAccountTypes.CREDIT_CARD]).toBe('Credit Card') - }) - - it('should handle CHECKING_LINE_OF_CREDIT as Checking', () => { - const name = AccountTypeNames[ReadableAccountTypes.CHECKING_LINE_OF_CREDIT] - expect(name).toBe('Checking') - }) - }) -}) diff --git a/src/const/jobDetailCode-test.tsx b/src/const/jobDetailCode-test.tsx deleted file mode 100644 index e1f51c6943..0000000000 --- a/src/const/jobDetailCode-test.tsx +++ /dev/null @@ -1,51 +0,0 @@ -import { JOB_DETAIL_CODE } from 'src/const/jobDetailCode' - -describe('JOB_DETAIL_CODE Constants', () => { - describe('Structure', () => { - it('should be an object', () => { - expect(typeof JOB_DETAIL_CODE).toBe('object') - expect(JOB_DETAIL_CODE).not.toBeNull() - }) - - it('should have exactly 1 property', () => { - expect(Object.keys(JOB_DETAIL_CODE)).toHaveLength(1) - }) - - it('should have all numeric values', () => { - Object.values(JOB_DETAIL_CODE).forEach((value) => { - expect(typeof value).toBe('number') - }) - }) - - it('should have unique values', () => { - const values = Object.values(JOB_DETAIL_CODE) - const uniqueValues = new Set(values) - expect(uniqueValues.size).toBe(values.length) - }) - }) - - describe('NO_VERIFIABLE_ACCOUNTS', () => { - it('should exist', () => { - expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBeDefined() - }) - - it('should equal 1000', () => { - expect(JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe(1000) - }) - - it('should be a number', () => { - expect(typeof JOB_DETAIL_CODE.NO_VERIFIABLE_ACCOUNTS).toBe('number') - }) - }) - - describe('Export', () => { - it('should export JOB_DETAIL_CODE as a named export', () => { - expect(JOB_DETAIL_CODE).toBeDefined() - }) - - it('should not be frozen or sealed', () => { - expect(Object.isFrozen(JOB_DETAIL_CODE)).toBe(false) - expect(Object.isSealed(JOB_DETAIL_CODE)).toBe(false) - }) - }) -})