From 5ff245f7d81bcaf70951ddaa52ff800dcc5a04d5 Mon Sep 17 00:00:00 2001 From: Nikhil-Doye Date: Mon, 20 Oct 2025 15:36:35 -0400 Subject: [PATCH] Add Jest configuration and testing utilities for improved test coverage - Introduced a Jest configuration file to set up the testing environment with jsdom and TypeScript support. - Added setupTests.ts to mock localStorage and console methods, enhancing test reliability and reducing noise. - Implemented comprehensive test cases for llmProcessor and copilotService, ensuring robust functionality and error handling. - Created unit tests for variable substitution utility, validating correct behavior for various scenarios. - Enhanced database connection manager tests to cover connection handling and credential validation. - Established a workflow store test suite to validate workflow management and node/edge operations. --- jest.config.js | 34 + package-lock.json | 1791 +++++++++++++++-- package.json | 8 + src/__tests__/processors/llmProcessor.test.ts | 414 ++++ src/__tests__/services/copilotService.test.ts | 331 +++ .../databaseConnectionManager.test.ts | 379 ++++ src/__tests__/store/workflowStore.test.ts | 312 +++ .../utils/variableSubstitution.simple.test.ts | 41 + .../utils/variableSubstitution.test.ts | 295 +++ src/setupTests.ts | 44 + 10 files changed, 3479 insertions(+), 170 deletions(-) create mode 100644 jest.config.js create mode 100644 src/__tests__/processors/llmProcessor.test.ts create mode 100644 src/__tests__/services/copilotService.test.ts create mode 100644 src/__tests__/services/databaseConnectionManager.test.ts create mode 100644 src/__tests__/store/workflowStore.test.ts create mode 100644 src/__tests__/utils/variableSubstitution.simple.test.ts create mode 100644 src/__tests__/utils/variableSubstitution.test.ts create mode 100644 src/setupTests.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..8ef3e85 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,34 @@ +module.exports = { + testEnvironment: "jsdom", + setupFilesAfterEnv: ["/src/setupTests.ts"], + moduleNameMapping: { + "^@/(.*)$": "/src/$1", + }, + collectCoverageFrom: [ + "src/**/*.{ts,tsx}", + "!src/**/*.d.ts", + "!src/index.tsx", + "!src/setupTests.ts", + "!src/test-*.ts", // Exclude manual test files + ], + coverageThreshold: { + global: { + branches: 70, + functions: 70, + lines: 70, + statements: 70, + }, + }, + testMatch: [ + "/src/**/__tests__/**/*.{ts,tsx}", + "/src/**/*.{test,spec}.{ts,tsx}", + ], + transform: { + "^.+\\.(ts|tsx)$": "ts-jest", + }, + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json"], + testPathIgnorePatterns: ["/node_modules/", "/build/"], + collectCoverage: true, + coverageDirectory: "coverage", + coverageReporters: ["text", "lcov", "html"], +}; diff --git a/package-lock.json b/package-lock.json index 8ea9539..fe6eab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -34,8 +34,21 @@ "typescript": "^4.9.0", "uuid": "^9.0.0", "zustand": "^4.4.1" + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "jest-environment-jsdom": "^30.2.0" } }, + "node_modules/@adobe/css-tools": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.4.tgz", + "integrity": "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg==", + "dev": true, + "license": "MIT" + }, "node_modules/@alloc/quick-lru": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@alloc/quick-lru/-/quick-lru-5.2.0.tgz", @@ -48,6 +61,27 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" + } + }, + "node_modules/@asamuzakjp/css-color/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/@babel/code-frame": { "version": "7.27.1", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", @@ -2092,6 +2126,121 @@ "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "license": "MIT" }, + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" + } + }, + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/@csstools/normalize.css": { "version": "12.1.1", "resolved": "https://registry.npmjs.org/@csstools/normalize.css/-/normalize.css-12.1.1.tgz", @@ -2731,6 +2880,255 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.2.0.tgz", + "integrity": "sha512-kazxw2L9IPuZpQ0mEt9lu9Z98SqR74xcagANmMBU16X0lS23yPc0+S6hGLUz8kVRlomZEs/5S/Zlpqwf5yu6OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.2.0", + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@jest/fake-timers": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz", @@ -2762,6 +3160,30 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jest/reporters": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-27.5.1.tgz", @@ -4143,6 +4565,107 @@ "url": "https://github.com/sponsors/gregberge" } }, + "node_modules/@testing-library/dom": { + "version": "10.4.1", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz", + "integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "picocolors": "1.1.1", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@testing-library/dom/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, + "node_modules/@testing-library/jest-dom": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", + "integrity": "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@adobe/css-tools": "^4.4.0", + "aria-query": "^5.0.0", + "css.escape": "^1.5.1", + "dom-accessibility-api": "^0.6.3", + "picocolors": "^1.1.1", + "redent": "^3.0.0" + }, + "engines": { + "node": ">=14", + "npm": ">=6", + "yarn": ">=1" + } + }, + "node_modules/@testing-library/jest-dom/node_modules/dom-accessibility-api": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.6.3.tgz", + "integrity": "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@testing-library/user-event": { + "version": "14.6.1", + "resolved": "https://registry.npmjs.org/@testing-library/user-event/-/user-event-14.6.1.tgz", + "integrity": "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12", + "npm": ">=6" + }, + "peerDependencies": { + "@testing-library/dom": ">=7.21.4" + } + }, "node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -4161,6 +4684,14 @@ "node": ">=10.13.0" } }, + "node_modules/@types/aria-query": { + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", + "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4615,6 +5146,18 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -4778,6 +5321,13 @@ "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", "license": "MIT" }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/trusted-types": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", @@ -5323,15 +5873,13 @@ } }, "node_modules/agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, "license": "MIT", - "dependencies": { - "debug": "4" - }, "engines": { - "node": ">= 6.0.0" + "node": ">= 14" } }, "node_modules/ajv": { @@ -7199,6 +7747,13 @@ "url": "https://github.com/sponsors/fb55" } }, + "node_modules/css.escape": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", + "integrity": "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==", + "dev": true, + "license": "MIT" + }, "node_modules/cssdb": { "version": "7.11.2", "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-7.11.2.tgz", @@ -7350,23 +7905,19 @@ "license": "MIT" }, "node_modules/cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", + "dev": true, "license": "MIT", "dependencies": { - "cssom": "~0.3.6" + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" }, "engines": { - "node": ">=8" + "node": ">=18" } }, - "node_modules/cssstyle/node_modules/cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "license": "MIT" - }, "node_modules/csstype": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", @@ -7485,17 +8036,17 @@ "license": "BSD-2-Clause" }, "node_modules/data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", + "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/data-view-buffer": { @@ -7666,6 +8217,17 @@ "node": ">= 0.8" } }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/destroy": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", @@ -7786,6 +8348,14 @@ "node": ">=6.0.0" } }, + "node_modules/dom-accessibility-api": { + "version": "0.5.16", + "resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", + "integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/dom-converter": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", @@ -10016,15 +10586,16 @@ } }, "node_modules/html-encoding-sniffer": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, "license": "MIT", "dependencies": { - "whatwg-encoding": "^1.0.5" + "whatwg-encoding": "^3.1.1" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/html-entities": { @@ -10164,17 +10735,17 @@ } }, "node_modules/http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, "license": "MIT", "dependencies": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" + "agent-base": "^7.1.0", + "debug": "^4.3.4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/http-proxy-middleware": { @@ -10202,16 +10773,17 @@ } }, "node_modules/https-proxy-agent": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", - "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, "license": "MIT", "dependencies": { - "agent-base": "6", + "agent-base": "^7.1.2", "debug": "4" }, "engines": { - "node": ">= 6" + "node": ">= 14" } }, "node_modules/human-signals": { @@ -10337,6 +10909,16 @@ "node": ">=0.8.19" } }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -11197,6 +11779,296 @@ } } }, + "node_modules/jest-config/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-config/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "license": "MIT" + }, + "node_modules/jest-config/node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-config/node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-config/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-config/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-config/node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-config/node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/jest-config/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-config/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-config/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jest-config/node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-config/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/jest-config/node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/jest-config/node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "license": "MIT" + }, + "node_modules/jest-config/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-config/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "license": "Apache-2.0" + }, "node_modules/jest-diff": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-27.5.1.tgz", @@ -11241,23 +12113,251 @@ } }, "node_modules/jest-environment-jsdom": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", - "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.2.0.tgz", + "integrity": "sha512-zbBTiqr2Vl78pKp/laGBREYzbZx9ZtqPjOK4++lL4BNDhxRnahg51HtoDrk9/VjIy9IthNEWdKVd7H5bqBhiWQ==", + "dev": true, "license": "MIT", "dependencies": { - "@jest/environment": "^27.5.1", - "@jest/fake-timers": "^27.5.1", - "@jest/types": "^27.5.1", + "@jest/environment": "30.2.0", + "@jest/environment-jsdom-abstract": "30.2.0", + "@types/jsdom": "^21.1.7", "@types/node": "*", - "jest-mock": "^27.5.1", - "jest-util": "^27.5.1", - "jsdom": "^16.6.0" + "jsdom": "^26.1.0" }, "engines": { - "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } } }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.2.0.tgz", + "integrity": "sha512-/QPTL7OBJQ5ac09UDRa3EQes4gt1FTEG/8jZ/4v5IVzx+Cv7dLxlVIvfvSVRiiX2drWyXeBjkMSR8hvOWSog5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.2.0", + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-mock": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.2.0.tgz", + "integrity": "sha512-HI3tRLjRxAbBy0VO8dqqm7Hb2mIa8d5bg/NJkyQcOk7V118ObQML8RC5luTF/Zsg4474a+gDvhce7eTnP4GhYw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/jest-environment-node": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-27.5.1.tgz", @@ -11492,6 +12592,296 @@ "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" } }, + "node_modules/jest-runner/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/jest-runner/node_modules/cssstyle": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", + "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "license": "MIT", + "dependencies": { + "cssom": "~0.3.6" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/cssstyle/node_modules/cssom": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", + "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/data-urls": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", + "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.3", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-runner/node_modules/html-encoding-sniffer": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", + "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^1.0.5" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-runner/node_modules/http-proxy-agent": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", + "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "license": "MIT", + "dependencies": { + "@tootallnate/once": "1", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/jest-runner/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-runner/node_modules/jest-environment-jsdom": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-27.5.1.tgz", + "integrity": "sha512-TFBvkTC1Hnnnrka/fUb56atfDtJ9VMZ94JkjTbggl1PEpwrYtUBKMezB3inLmWqQsXYLcMwNoDQwoBTAvFfsfw==", + "license": "MIT", + "dependencies": { + "@jest/environment": "^27.5.1", + "@jest/fake-timers": "^27.5.1", + "@jest/types": "^27.5.1", + "@types/node": "*", + "jest-mock": "^27.5.1", + "jest-util": "^27.5.1", + "jsdom": "^16.6.0" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0" + } + }, + "node_modules/jest-runner/node_modules/jsdom": { + "version": "16.7.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", + "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "license": "MIT", + "dependencies": { + "abab": "^2.0.5", + "acorn": "^8.2.4", + "acorn-globals": "^6.0.0", + "cssom": "^0.4.4", + "cssstyle": "^2.3.0", + "data-urls": "^2.0.0", + "decimal.js": "^10.2.1", + "domexception": "^2.0.1", + "escodegen": "^2.0.0", + "form-data": "^3.0.0", + "html-encoding-sniffer": "^2.0.1", + "http-proxy-agent": "^4.0.1", + "https-proxy-agent": "^5.0.0", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.0", + "parse5": "6.0.1", + "saxes": "^5.0.1", + "symbol-tree": "^3.2.4", + "tough-cookie": "^4.0.0", + "w3c-hr-time": "^1.0.2", + "w3c-xmlserializer": "^2.0.0", + "webidl-conversions": "^6.1.0", + "whatwg-encoding": "^1.0.5", + "whatwg-mimetype": "^2.3.0", + "whatwg-url": "^8.5.0", + "ws": "^7.4.6", + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "canvas": "^2.5.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-runner/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/saxes": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", + "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-runner/node_modules/tough-cookie": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", + "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "license": "BSD-3-Clause", + "dependencies": { + "psl": "^1.1.33", + "punycode": "^2.1.1", + "universalify": "^0.2.0", + "url-parse": "^1.5.3" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jest-runner/node_modules/tr46": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", + "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "license": "MIT", + "dependencies": { + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/universalify": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", + "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/jest-runner/node_modules/w3c-xmlserializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", + "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "license": "MIT", + "dependencies": { + "xml-name-validator": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-runner/node_modules/webidl-conversions": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", + "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=10.4" + } + }, + "node_modules/jest-runner/node_modules/whatwg-encoding": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", + "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.4.24" + } + }, + "node_modules/jest-runner/node_modules/whatwg-mimetype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", + "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", + "license": "MIT" + }, + "node_modules/jest-runner/node_modules/whatwg-url": { + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", + "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "license": "MIT", + "dependencies": { + "lodash": "^4.7.0", + "tr46": "^2.1.0", + "webidl-conversions": "^6.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-runner/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jest-runner/node_modules/xml-name-validator": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", + "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", + "license": "Apache-2.0" + }, "node_modules/jest-runtime": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-27.5.1.tgz", @@ -11977,44 +13367,38 @@ } }, "node_modules/jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, "license": "MIT", "dependencies": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" }, "peerDependencies": { - "canvas": "^2.5.0" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -12022,27 +13406,6 @@ } } }, - "node_modules/jsdom/node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -12380,6 +13743,17 @@ "node": ">=12" } }, + "node_modules/lz-string": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", + "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "lz-string": "bin/bin.js" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -12546,6 +13920,16 @@ "node": ">=6" } }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.4", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", @@ -13144,10 +14528,30 @@ } }, "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "license": "MIT" + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/parseurl": { "version": "1.3.3", @@ -15264,6 +16668,20 @@ "node": ">=6.0.0" } }, + "node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -15622,6 +17040,13 @@ "randombytes": "^2.1.0" } }, + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", + "dev": true, + "license": "MIT" + }, "node_modules/run-parallel": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", @@ -15774,15 +17199,16 @@ "license": "ISC" }, "node_modules/saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, "license": "ISC", "dependencies": { "xmlchars": "^2.2.0" }, "engines": { - "node": ">=10" + "node": ">=v12.22.7" } }, "node_modules/scheduler": { @@ -16731,6 +18157,19 @@ "node": ">=6" } }, + "node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -17346,6 +18785,26 @@ "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "license": "MIT" }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" + } + }, + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, "node_modules/tmpl": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", @@ -17374,39 +18833,29 @@ } }, "node_modules/tough-cookie": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz", - "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==", + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", + "dev": true, "license": "BSD-3-Clause", "dependencies": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.2.0", - "url-parse": "^1.5.3" + "tldts": "^6.1.32" }, "engines": { - "node": ">=6" - } - }, - "node_modules/tough-cookie/node_modules/universalify": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz", - "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==", - "license": "MIT", - "engines": { - "node": ">= 4.0.0" + "node": ">=16" } }, "node_modules/tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, "license": "MIT", "dependencies": { - "punycode": "^2.1.1" + "punycode": "^2.3.1" }, "engines": { - "node": ">=8" + "node": ">=18" } }, "node_modules/tryer": { @@ -17929,15 +19378,16 @@ } }, "node_modules/w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", + "dev": true, "license": "MIT", "dependencies": { - "xml-name-validator": "^3.0.0" + "xml-name-validator": "^5.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/walker": { @@ -17972,12 +19422,13 @@ } }, "node_modules/webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, "license": "BSD-2-Clause", "engines": { - "node": ">=10.4" + "node": ">=12" } }, "node_modules/webpack": { @@ -18203,24 +19654,16 @@ } }, "node_modules/whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "dev": true, "license": "MIT", "dependencies": { - "iconv-lite": "0.4.24" - } - }, - "node_modules/whatwg-encoding/node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" + "iconv-lite": "0.6.3" }, "engines": { - "node": ">=0.10.0" + "node": ">=18" } }, "node_modules/whatwg-fetch": { @@ -18230,23 +19673,27 @@ "license": "MIT" }, "node_modules/whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "license": "MIT" + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } }, "node_modules/whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, "license": "MIT", "dependencies": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" }, "engines": { - "node": ">=10" + "node": ">=18" } }, "node_modules/which": { @@ -18757,10 +20204,14 @@ } }, "node_modules/xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "license": "Apache-2.0" + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } }, "node_modules/xmlchars": { "version": "2.2.0", diff --git a/package.json b/package.json index e259c17..ef08845 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,8 @@ "start": "react-scripts start", "build": "react-scripts build", "test": "react-scripts test", + "test:coverage": "react-scripts test --coverage --watchAll=false", + "test:ci": "react-scripts test --coverage --watchAll=false --ci", "eject": "react-scripts eject" }, "eslintConfig": { @@ -54,5 +56,11 @@ "last 1 firefox version", "last 1 safari version" ] + }, + "devDependencies": { + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.0", + "@testing-library/user-event": "^14.6.1", + "jest-environment-jsdom": "^30.2.0" } } diff --git a/src/__tests__/processors/llmProcessor.test.ts b/src/__tests__/processors/llmProcessor.test.ts new file mode 100644 index 0000000..f4178b5 --- /dev/null +++ b/src/__tests__/processors/llmProcessor.test.ts @@ -0,0 +1,414 @@ +import llmProcessor from '../../services/processors/llmProcessor'; +import { ExecutionContext, ExecutionPlan } from '../../services/executionEngine'; + +// Mock dependencies +jest.mock('../../services/openaiService', () => ({ + callOpenAI: jest.fn(), +})); + +jest.mock('../../utils/variableSubstitution', () => ({ + substituteVariables: jest.fn(), +})); + +describe('LLM Processor', () => { + let mockCallOpenAI: jest.MockedFunction; + let mockSubstituteVariables: jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + + mockCallOpenAI = require('../../services/openaiService').callOpenAI; + mockSubstituteVariables = require('../../utils/variableSubstitution').substituteVariables; + }); + + describe('Basic Processing', () => { + it('should process text with basic prompt', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Analyze this text: {{input}}', + model: 'gpt-3.5-turbo', + temperature: 0.7, + }, + inputs: new Map([['input', 'Hello world']]), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: 'Analysis: The text "Hello world" is a common greeting.', + usage: { total_tokens: 50 }, + }); + + mockSubstituteVariables.mockReturnValue('Analyze this text: Hello world'); + + const result = await llmProcessor(context, plan); + + expect(result.success).toBe(true); + expect(result.output).toBe('Analysis: The text "Hello world" is a common greeting.'); + expect(result.model).toBe('gpt-3.5-turbo'); + expect(mockCallOpenAI).toHaveBeenCalledWith( + 'Analyze this text: Hello world', + { + model: 'gpt-3.5-turbo', + temperature: 0.7, + maxTokens: 1000, + } + ); + }); + + it('should use default model when not specified', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Simple prompt', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: 'Response', + usage: { total_tokens: 10 }, + }); + + mockSubstituteVariables.mockReturnValue('Simple prompt'); + + await llmProcessor(context, plan); + + expect(mockCallOpenAI).toHaveBeenCalledWith( + 'Simple prompt', + { + model: 'gpt-3.5-turbo', + temperature: 0.7, + maxTokens: 1000, + } + ); + }); + + it('should fall back to input data when no prompt provided', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: {}, + inputs: new Map([['input', 'Fallback input data']]), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: 'Processed: Fallback input data', + usage: { total_tokens: 20 }, + }); + + mockSubstituteVariables.mockReturnValue('Fallback input data'); + + const result = await llmProcessor(context, plan); + + expect(result.input).toBe('Fallback input data'); + expect(result.output).toBe('Processed: Fallback input data'); + }); + }); + + describe('Variable Substitution', () => { + it('should apply variable substitution to prompts', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Process {{node-0.output}} and {{node-1.data}}', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + // Mock node label to ID mapping + (plan as any).nodeLabelToId = new Map([ + ['node-0', 'uuid-1'], + ['node-1', 'uuid-2'], + ]); + + mockCallOpenAI.mockResolvedValue({ + content: 'Processed with variables', + usage: { total_tokens: 30 }, + }); + + mockSubstituteVariables.mockReturnValue('Process data1 and data2'); + + await llmProcessor(context, plan); + + expect(mockSubstituteVariables).toHaveBeenCalledWith( + 'Process {{node-0.output}} and {{node-1.data}}', + expect.any(Map), + expect.any(Map) + ); + }); + + it('should skip substitution when prompt has no variables', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Simple prompt without variables', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: 'Response', + usage: { total_tokens: 10 }, + }); + + mockSubstituteVariables.mockReturnValue('Simple prompt without variables'); + + await llmProcessor(context, plan); + + expect(mockSubstituteVariables).toHaveBeenCalledTimes(1); + }); + }); + + describe('Error Handling', () => { + it('should handle OpenAI API errors', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Test prompt', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockRejectedValue(new Error('API rate limit exceeded')); + mockSubstituteVariables.mockReturnValue('Test prompt'); + + await expect(llmProcessor(context, plan)).rejects.toThrow( + 'LLM processing failed: API rate limit exceeded' + ); + }); + + it('should handle variable substitution errors', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Process {{invalid.variable}}', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockSubstituteVariables.mockImplementation(() => { + throw new Error('Variable substitution failed'); + }); + + await expect(llmProcessor(context, plan)).rejects.toThrow( + 'LLM processing failed: Variable substitution failed' + ); + }); + + it('should handle empty response from OpenAI', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Test prompt', + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: '', + usage: { total_tokens: 0 }, + }); + + mockSubstituteVariables.mockReturnValue('Test prompt'); + + const result = await llmProcessor(context, plan); + + expect(result.success).toBe(true); + expect(result.output).toBe(''); + }); + }); + + describe('Configuration Options', () => { + it('should use custom temperature and max tokens', async () => { + const context: ExecutionContext = { + nodeId: 'node-1', + nodeType: 'llmTask', + config: { + prompt: 'Test prompt', + model: 'gpt-4', + temperature: 0.9, + maxTokens: 2000, + }, + inputs: new Map(), + outputs: new Map(), + status: 'pending', + retryCount: 0, + maxRetries: 3, + }; + + const plan: ExecutionPlan = { + id: 'plan-1', + workflowId: 'workflow-1', + nodes: [context], + edges: [], + executionMode: 'sequential', + parallelGroups: [], + conditions: new Map(), + status: 'running', + results: new Map(), + errors: new Map(), + }; + + mockCallOpenAI.mockResolvedValue({ + content: 'Response', + usage: { total_tokens: 100 }, + }); + + mockSubstituteVariables.mockReturnValue('Test prompt'); + + await llmProcessor(context, plan); + + expect(mockCallOpenAI).toHaveBeenCalledWith( + 'Test prompt', + { + model: 'gpt-4', + temperature: 0.9, + maxTokens: 2000, + } + ); + }); + }); +}); diff --git a/src/__tests__/services/copilotService.test.ts b/src/__tests__/services/copilotService.test.ts new file mode 100644 index 0000000..6265fb6 --- /dev/null +++ b/src/__tests__/services/copilotService.test.ts @@ -0,0 +1,331 @@ +import { copilotService } from "../../services/copilotService"; + +// Mock dependencies +jest.mock("../../services/openaiService", () => ({ + callOpenAI: jest.fn(), +})); + +jest.mock("../../services/agents/AgentManager", () => ({ + agentManager: { + generateWorkflowFromDescription: jest.fn(), + }, +})); + +jest.mock("../../utils/workflowGenerator", () => ({ + generateWorkflowStructure: jest.fn(), +})); + +jest.mock("../../utils/workflowValidator", () => ({ + validateWorkflowStructure: jest.fn(), +})); + +describe("CopilotService", () => { + let mockCallOpenAI: jest.MockedFunction; + let mockAgentManager: any; + let mockGenerateWorkflowStructure: jest.MockedFunction; + let mockValidateWorkflowStructure: jest.MockedFunction; + + beforeEach(() => { + jest.clearAllMocks(); + + mockCallOpenAI = require("../../services/openaiService").callOpenAI; + mockAgentManager = + require("../../services/agents/AgentManager").agentManager; + mockGenerateWorkflowStructure = + require("../../utils/workflowGenerator").generateWorkflowStructure; + mockValidateWorkflowStructure = + require("../../utils/workflowValidator").validateWorkflowStructure; + }); + + describe("parseUserIntent", () => { + it("should parse user intent successfully", async () => { + const mockResponse = { + content: JSON.stringify({ + intent: "WEB_SCRAPING", + entities: { + urls: ["https://example.com"], + dataTypes: ["html"], + }, + workflowStructure: { + nodes: [ + { type: "dataInput", label: "URL Input" }, + { type: "webScraping", label: "Web Scraper" }, + ], + edges: [{ source: "node-0", target: "node-1" }], + }, + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const result = await copilotService.parseUserIntent( + "Scrape data from https://example.com" + ); + + expect(result.intent).toBe("WEB_SCRAPING"); + expect(result.entities.urls).toContain("https://example.com"); + expect(result.workflowStructure).toBeDefined(); + expect(mockCallOpenAI).toHaveBeenCalledWith( + expect.stringContaining("Scrape data from https://example.com"), + expect.any(Object) + ); + }); + + it("should handle parsing errors gracefully", async () => { + mockCallOpenAI.mockRejectedValue(new Error("API Error")); + + await expect( + copilotService.parseUserIntent("Invalid input") + ).rejects.toThrow("API Error"); + }); + + it("should use cache for repeated requests", async () => { + const mockResponse = { + content: JSON.stringify({ + intent: "DATA_PROCESSING", + entities: {}, + workflowStructure: null, + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + // First call + await copilotService.parseUserIntent("Process some data"); + expect(mockCallOpenAI).toHaveBeenCalledTimes(1); + + // Second call should use cache + await copilotService.parseUserIntent("Process some data"); + expect(mockCallOpenAI).toHaveBeenCalledTimes(1); + }); + }); + + describe("generateWorkflowSuggestions", () => { + it("should generate workflow suggestions", async () => { + const mockResponse = { + content: JSON.stringify({ + suggestions: [ + "Add a data validation step", + "Include error handling", + "Add data transformation", + ], + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const suggestions = await copilotService.generateWorkflowSuggestions( + "web scraping workflow", + { nodes: [], edges: [] } + ); + + expect(suggestions).toHaveLength(3); + expect(suggestions[0]).toBe("Add a data validation step"); + expect(mockCallOpenAI).toHaveBeenCalledWith( + expect.stringContaining("web scraping workflow"), + expect.any(Object) + ); + }); + + it("should handle empty suggestions", async () => { + const mockResponse = { + content: JSON.stringify({ + suggestions: [], + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const suggestions = await copilotService.generateWorkflowSuggestions( + "simple workflow", + { nodes: [], edges: [] } + ); + + expect(suggestions).toHaveLength(0); + }); + }); + + describe("generateWorkflowFromDescription", () => { + it("should generate workflow from description", async () => { + const mockWorkflowStructure = { + nodes: [ + { type: "dataInput", label: "Input" }, + { type: "llmTask", label: "Processor" }, + ], + edges: [{ source: "node-0", target: "node-1" }], + }; + + mockAgentManager.generateWorkflowFromDescription.mockResolvedValue({ + intent: "AI_ANALYSIS", + entities: {}, + workflowStructure: mockWorkflowStructure, + }); + + const result = await copilotService.generateWorkflowFromDescription( + "Analyze text data with AI" + ); + + expect(result.intent).toBe("AI_ANALYSIS"); + expect(result.workflowStructure).toEqual(mockWorkflowStructure); + expect( + mockAgentManager.generateWorkflowFromDescription + ).toHaveBeenCalledWith("Analyze text data with AI"); + }); + + it("should handle generation errors", async () => { + mockAgentManager.generateWorkflowFromDescription.mockRejectedValue( + new Error("Generation failed") + ); + + await expect( + copilotService.generateWorkflowFromDescription("Invalid request") + ).rejects.toThrow("Generation failed"); + }); + }); + + describe("validateWorkflow", () => { + it("should validate workflow structure", async () => { + const mockWorkflow = { + nodes: [ + { type: "dataInput", label: "Input" }, + { type: "llmTask", label: "Processor" }, + ], + edges: [{ source: "node-0", target: "node-1" }], + }; + + mockValidateWorkflowStructure.mockReturnValue({ + isValid: true, + issues: [], + suggestions: [], + }); + + const result = await copilotService.validateWorkflow(mockWorkflow); + + expect(result.isValid).toBe(true); + expect(result.issues).toHaveLength(0); + expect(mockValidateWorkflowStructure).toHaveBeenCalledWith(mockWorkflow); + }); + + it("should identify validation issues", async () => { + const mockWorkflow = { + nodes: [ + { type: "dataInput", label: "Input" }, + // Missing processor node + ], + edges: [], + }; + + mockValidateWorkflowStructure.mockReturnValue({ + isValid: false, + issues: ["No processing nodes found"], + suggestions: ["Add a processing node"], + }); + + const result = await copilotService.validateWorkflow(mockWorkflow); + + expect(result.isValid).toBe(false); + expect(result.issues).toContain("No processing nodes found"); + expect(result.suggestions).toContain("Add a processing node"); + }); + }); + + describe("optimizeWorkflow", () => { + it("should optimize workflow performance", async () => { + const mockResponse = { + content: JSON.stringify({ + optimizations: [ + "Add parallel processing for independent nodes", + "Implement caching for repeated operations", + "Optimize data flow between nodes", + ], + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const optimizations = await copilotService.optimizeWorkflow({ + nodes: [], + edges: [], + }); + + expect(optimizations).toHaveLength(3); + expect(optimizations[0]).toBe( + "Add parallel processing for independent nodes" + ); + }); + }); + + describe("getCopilotSuggestions", () => { + it("should get contextual suggestions", async () => { + const mockResponse = { + content: JSON.stringify({ + suggestions: [ + "Consider adding error handling", + "Add data validation step", + "Include progress indicators", + ], + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const suggestions = await copilotService.getCopilotSuggestions( + "web scraping workflow" + ); + + expect(suggestions).toHaveLength(3); + expect(suggestions[0]).toBe("Consider adding error handling"); + }); + + it("should handle context-specific suggestions", async () => { + const mockResponse = { + content: JSON.stringify({ + suggestions: [ + "Add database connection for data storage", + "Include data transformation steps", + "Add output formatting options", + ], + }), + }; + + mockCallOpenAI.mockResolvedValue(mockResponse); + + const suggestions = await copilotService.getCopilotSuggestions( + "data processing workflow with database" + ); + + expect(suggestions).toHaveLength(3); + expect(suggestions[0]).toBe("Add database connection for data storage"); + }); + }); + + describe("Error Handling", () => { + it("should handle malformed JSON responses", async () => { + mockCallOpenAI.mockResolvedValue({ + content: "Invalid JSON response", + }); + + await expect( + copilotService.parseUserIntent("Test input") + ).rejects.toThrow(); + }); + + it("should handle network timeouts", async () => { + mockCallOpenAI.mockRejectedValue(new Error("Request timeout")); + + await expect( + copilotService.parseUserIntent("Test input") + ).rejects.toThrow("Request timeout"); + }); + + it("should handle empty responses", async () => { + mockCallOpenAI.mockResolvedValue({ + content: "", + }); + + await expect( + copilotService.parseUserIntent("Test input") + ).rejects.toThrow(); + }); + }); +}); diff --git a/src/__tests__/services/databaseConnectionManager.test.ts b/src/__tests__/services/databaseConnectionManager.test.ts new file mode 100644 index 0000000..3ea693f --- /dev/null +++ b/src/__tests__/services/databaseConnectionManager.test.ts @@ -0,0 +1,379 @@ +import { databaseConnectionManager } from '../../services/databaseConnectionManager'; + +// Mock localStorage +const mockLocalStorage = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; + +Object.defineProperty(window, 'localStorage', { + value: mockLocalStorage, +}); + +// Mock connector classes +jest.mock('../../connectors/database/MongoConnector', () => ({ + MongoConnector: jest.fn().mockImplementation(() => ({ + testConnection: jest.fn().mockResolvedValue(true), + connect: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + })), +})); + +jest.mock('../../connectors/database/MySQLConnector', () => ({ + MySQLConnector: jest.fn().mockImplementation(() => ({ + testConnection: jest.fn().mockResolvedValue(true), + connect: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + })), +})); + +jest.mock('../../connectors/database/PostgresConnector', () => ({ + PostgresConnector: jest.fn().mockImplementation(() => ({ + testConnection: jest.fn().mockResolvedValue(true), + connect: jest.fn().mockResolvedValue(true), + disconnect: jest.fn().mockResolvedValue(undefined), + })), +})); + +describe('DatabaseConnectionManager', () => { + beforeEach(() => { + jest.clearAllMocks(); + mockLocalStorage.getItem.mockReturnValue(null); + }); + + describe('Connection Management', () => { + it('should add a MongoDB connection', async () => { + const connectionData = { + name: 'Test MongoDB', + type: 'mongodb' as const, + host: 'localhost', + port: 27017, + database: 'testdb', + connectionString: 'mongodb://localhost:27017/testdb', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + + expect(connectionId).toBeDefined(); + expect(connectionId).toMatch(/^conn_/); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection).toBeDefined(); + expect(connection!.name).toBe('Test MongoDB'); + expect(connection!.type).toBe('mongodb'); + }); + + it('should add a MySQL connection', async () => { + const connectionData = { + name: 'Test MySQL', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + + expect(connectionId).toBeDefined(); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection).toBeDefined(); + expect(connection!.name).toBe('Test MySQL'); + expect(connection!.type).toBe('mysql'); + }); + + it('should add a PostgreSQL connection', async () => { + const connectionData = { + name: 'Test PostgreSQL', + type: 'postgresql' as const, + host: 'localhost', + port: 5432, + database: 'testdb', + username: 'postgres', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + + expect(connectionId).toBeDefined(); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection).toBeDefined(); + expect(connection!.name).toBe('Test PostgreSQL'); + expect(connection!.type).toBe('postgresql'); + }); + + it('should update a connection', async () => { + const connectionData = { + name: 'Test Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + + const updated = await databaseConnectionManager.updateConnection(connectionId, { + name: 'Updated Connection', + port: 3307, + }); + + expect(updated).toBe(true); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection!.name).toBe('Updated Connection'); + expect(connection!.port).toBe(3307); + }); + + it('should delete a connection', async () => { + const connectionData = { + name: 'Test Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + const deleted = await databaseConnectionManager.deleteConnection(connectionId); + + expect(deleted).toBe(true); + expect(databaseConnectionManager.getConnection(connectionId)).toBeUndefined(); + }); + }); + + describe('Credential Validation', () => { + it('should validate MongoDB credentials', async () => { + const invalidConnection = { + name: 'Invalid MongoDB', + type: 'mongodb' as const, + host: 'localhost', + port: 27017, + database: 'testdb', + // Missing connectionString + }; + + await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow( + 'MongoDB requires a connection string' + ); + }); + + it('should validate MySQL credentials', async () => { + const invalidConnection = { + name: 'Invalid MySQL', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + // Missing username and password + }; + + await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow( + 'MySQL connection validation failed' + ); + }); + + it('should validate PostgreSQL credentials', async () => { + const invalidConnection = { + name: 'Invalid PostgreSQL', + type: 'postgresql' as const, + host: 'localhost', + port: 5432, + database: 'testdb', + // Missing username and password + }; + + await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow( + 'PostgreSQL connection validation failed' + ); + }); + + it('should validate port ranges', async () => { + const invalidConnection = { + name: 'Invalid Port', + type: 'mysql' as const, + host: 'localhost', + port: 99999, // Invalid port + database: 'testdb', + username: 'root', + password: 'password', + }; + + await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow( + 'Valid port number (1-65535) is required' + ); + }); + + it('should validate MongoDB connection string format', async () => { + const invalidConnection = { + name: 'Invalid MongoDB', + type: 'mongodb' as const, + host: 'localhost', + port: 27017, + database: 'testdb', + connectionString: 'invalid-connection-string', + }; + + await expect(databaseConnectionManager.addConnection(invalidConnection)).rejects.toThrow( + 'Invalid MongoDB connection string format' + ); + }); + }); + + describe('Connection Testing', () => { + it('should test a connection successfully', async () => { + const connectionData = { + name: 'Test Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + const result = await databaseConnectionManager.testConnection(connectionId); + + expect(result.success).toBe(true); + expect(result.message).toBe('Connection successful'); + expect(result.executionTime).toBeGreaterThan(0); + }); + + it('should handle connection test failure', async () => { + // Mock connector to return false for testConnection + const { MongoConnector } = require('../../connectors/database/MongoConnector'); + MongoConnector.mockImplementation(() => ({ + testConnection: jest.fn().mockResolvedValue(false), + connect: jest.fn().mockResolvedValue(false), + disconnect: jest.fn().mockResolvedValue(undefined), + })); + + const connectionData = { + name: 'Test Connection', + type: 'mongodb' as const, + host: 'localhost', + port: 27017, + database: 'testdb', + connectionString: 'mongodb://localhost:27017/testdb', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + const result = await databaseConnectionManager.testConnection(connectionId); + + expect(result.success).toBe(false); + expect(result.message).toBe('Connection test failed'); + }); + + it('should handle connection test error', async () => { + // Mock connector to throw an error + const { MongoConnector } = require('../../connectors/database/MongoConnector'); + MongoConnector.mockImplementation(() => ({ + testConnection: jest.fn().mockRejectedValue(new Error('Connection failed')), + connect: jest.fn().mockRejectedValue(new Error('Connection failed')), + disconnect: jest.fn().mockResolvedValue(undefined), + })); + + const connectionData = { + name: 'Test Connection', + type: 'mongodb' as const, + host: 'localhost', + port: 27017, + database: 'testdb', + connectionString: 'mongodb://localhost:27017/testdb', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + const result = await databaseConnectionManager.testConnection(connectionId); + + expect(result.success).toBe(false); + expect(result.message).toBe('Connection failed'); + }); + }); + + describe('Connection Operations', () => { + it('should connect to database', async () => { + const connectionData = { + name: 'Test Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + const connected = await databaseConnectionManager.connect(connectionId); + + expect(connected).toBe(true); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection!.status).toBe('connected'); + }); + + it('should disconnect from database', async () => { + const connectionData = { + name: 'Test Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const connectionId = await databaseConnectionManager.addConnection(connectionData); + await databaseConnectionManager.connect(connectionId); + + const disconnected = await databaseConnectionManager.disconnect(connectionId); + + expect(disconnected).toBe(true); + + const connection = databaseConnectionManager.getConnection(connectionId); + expect(connection!.status).toBe('disconnected'); + }); + + it('should get connections by type', async () => { + const mysqlConnection = { + name: 'MySQL Connection', + type: 'mysql' as const, + host: 'localhost', + port: 3306, + database: 'testdb', + username: 'root', + password: 'password', + }; + + const postgresConnection = { + name: 'PostgreSQL Connection', + type: 'postgresql' as const, + host: 'localhost', + port: 5432, + database: 'testdb', + username: 'postgres', + password: 'password', + }; + + await databaseConnectionManager.addConnection(mysqlConnection); + await databaseConnectionManager.addConnection(postgresConnection); + + const mysqlConnections = databaseConnectionManager.getConnectionsByType('mysql'); + const postgresConnections = databaseConnectionManager.getConnectionsByType('postgresql'); + + expect(mysqlConnections).toHaveLength(1); + expect(postgresConnections).toHaveLength(1); + expect(mysqlConnections[0].name).toBe('MySQL Connection'); + expect(postgresConnections[0].name).toBe('PostgreSQL Connection'); + }); + }); +}); diff --git a/src/__tests__/store/workflowStore.test.ts b/src/__tests__/store/workflowStore.test.ts new file mode 100644 index 0000000..4b67488 --- /dev/null +++ b/src/__tests__/store/workflowStore.test.ts @@ -0,0 +1,312 @@ +import { renderHook, act } from "@testing-library/react"; +import { useWorkflowStore } from "../../store/workflowStore"; +import { Workflow, WorkflowNode, WorkflowEdge } from "../../types"; + +// Mock localStorage +const mockLocalStorage = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; + +Object.defineProperty(window, "localStorage", { + value: mockLocalStorage, +}); + +describe("WorkflowStore", () => { + beforeEach(() => { + jest.clearAllMocks(); + mockLocalStorage.getItem.mockReturnValue(null); + + // Reset the store state before each test + const { result } = renderHook(() => useWorkflowStore()); + act(() => { + result.current.clearAllWorkflows(); + }); + }); + + describe("Workflow Management", () => { + it("should create a new workflow", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + }); + + expect(result.current.workflows).toHaveLength(1); + expect(result.current.workflows[0].name).toBe("Test Workflow"); + expect(result.current.currentWorkflow).toBe(result.current.workflows[0]); + }); + + it("should load workflows from localStorage", () => { + const mockWorkflows = [ + { + id: "workflow-1", + name: "Saved Workflow", + nodes: [], + edges: [], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }, + ]; + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify(mockWorkflows)); + + const { result } = renderHook(() => useWorkflowStore()); + + expect(result.current.workflows).toHaveLength(1); + expect(result.current.workflows[0].name).toBe("Saved Workflow"); + }); + + it("should update workflow", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Original Name"); + }); + + const workflowId = result.current.currentWorkflow!.id; + + act(() => { + result.current.updateWorkflow(workflowId, { name: "Updated Name" }); + }); + + expect(result.current.currentWorkflow!.name).toBe("Updated Name"); + expect(mockLocalStorage.setItem).toHaveBeenCalled(); + }); + + it("should delete workflow", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Workflow 1"); + result.current.createWorkflow("Workflow 2"); + }); + + const workflowId = result.current.workflows[0].id; + + act(() => { + result.current.deleteWorkflow(workflowId); + }); + + expect(result.current.workflows).toHaveLength(1); + expect(result.current.workflows[0].name).toBe("Workflow 2"); + }); + }); + + describe("Node Management", () => { + it("should add a node", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + }); + + expect(result.current.currentWorkflow!.nodes).toHaveLength(1); + expect(result.current.currentWorkflow!.nodes[0].type).toBe("dataInput"); + expect(result.current.currentWorkflow!.nodes[0].data.id).toBe( + result.current.currentWorkflow!.nodes[0].id + ); + }); + + it("should update a node", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + }); + + const nodeId = result.current.currentWorkflow!.nodes[0].id; + + act(() => { + result.current.updateNode(nodeId, { label: "Updated Label" }); + }); + + expect(result.current.currentWorkflow!.nodes[0].data.label).toBe( + "Updated Label" + ); + }); + + it("should delete a node", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + result.current.addNode("llmTask", { x: 200, y: 200 }); + }); + + const nodeId = result.current.currentWorkflow!.nodes[0].id; + + act(() => { + result.current.deleteNode(nodeId); + }); + + expect(result.current.currentWorkflow!.nodes).toHaveLength(1); + expect(result.current.currentWorkflow!.nodes[0].type).toBe("llmTask"); + }); + + it("should validate unique node labels", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + result.current.addNode("llmTask", { x: 200, y: 200 }); + }); + + const firstNodeId = result.current.currentWorkflow!.nodes[0].id; + const secondNodeId = result.current.currentWorkflow!.nodes[1].id; + + act(() => { + result.current.updateNode(firstNodeId, { label: "Test Label" }); + result.current.updateNode(secondNodeId, { label: "Test Label" }); + }); + + // Should warn about duplicate labels + expect(console.warn).toHaveBeenCalledWith( + expect.stringContaining('Node label "Test Label" already exists') + ); + }); + }); + + describe("Edge Management", () => { + it("should add an edge", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + result.current.addNode("llmTask", { x: 200, y: 200 }); + }); + + const sourceId = result.current.currentWorkflow!.nodes[0].id; + const targetId = result.current.currentWorkflow!.nodes[1].id; + + act(() => { + result.current.addEdge(sourceId, targetId); + }); + + expect(result.current.currentWorkflow!.edges).toHaveLength(1); + expect(result.current.currentWorkflow!.edges[0].source).toBe(sourceId); + expect(result.current.currentWorkflow!.edges[0].target).toBe(targetId); + }); + + it("should delete an edge", () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + result.current.addNode("llmTask", { x: 200, y: 200 }); + }); + + const sourceId = result.current.currentWorkflow!.nodes[0].id; + const targetId = result.current.currentWorkflow!.nodes[1].id; + + act(() => { + result.current.addEdge(sourceId, targetId); + }); + + const edgeId = result.current.currentWorkflow!.edges[0].id; + + act(() => { + result.current.deleteEdge(edgeId); + }); + + expect(result.current.currentWorkflow!.edges).toHaveLength(0); + }); + }); + + describe("ID Migration", () => { + it("should migrate deterministic IDs to UUIDs", () => { + const mockWorkflow = { + id: "workflow-1", + name: "Test Workflow", + nodes: [ + { id: "node-0", data: { id: "node-0", label: "Input Node" } }, + { id: "node-1", data: { id: "node-1", label: "Process Node" } }, + ], + edges: [{ id: "edge-0", source: "node-0", target: "node-1" }], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockWorkflow])); + + const { result } = renderHook(() => useWorkflowStore()); + + const workflow = result.current.workflows[0]; + + // Check that deterministic IDs were migrated to UUIDs + expect(workflow.nodes[0].id).not.toMatch(/^node-\d+$/); + expect(workflow.nodes[1].id).not.toMatch(/^node-\d+$/); + expect(workflow.nodes[0].data.id).toBe(workflow.nodes[0].id); + expect(workflow.nodes[1].data.id).toBe(workflow.nodes[1].id); + + // Check that edges reference the new UUIDs + expect(workflow.edges[0].source).toBe(workflow.nodes[0].id); + expect(workflow.edges[0].target).toBe(workflow.nodes[1].id); + }); + + it("should not migrate already migrated workflows", () => { + const mockWorkflow = { + id: "workflow-1", + name: "Test Workflow", + nodes: [ + { id: "uuid-1", data: { id: "uuid-1", label: "Input Node" } }, + { id: "uuid-2", data: { id: "uuid-2", label: "Process Node" } }, + ], + edges: [{ id: "edge-uuid", source: "uuid-1", target: "uuid-2" }], + createdAt: new Date().toISOString(), + updatedAt: new Date().toISOString(), + }; + + mockLocalStorage.getItem.mockReturnValue(JSON.stringify([mockWorkflow])); + + const { result } = renderHook(() => useWorkflowStore()); + + const workflow = result.current.workflows[0]; + + // Should remain unchanged + expect(workflow.nodes[0].id).toBe("uuid-1"); + expect(workflow.nodes[1].id).toBe("uuid-2"); + expect(workflow.edges[0].source).toBe("uuid-1"); + expect(workflow.edges[0].target).toBe("uuid-2"); + }); + }); + + describe("Execution", () => { + it("should execute workflow", async () => { + const { result } = renderHook(() => useWorkflowStore()); + + act(() => { + result.current.createWorkflow("Test Workflow"); + result.current.addNode("dataInput", { x: 100, y: 100 }); + }); + + // Mock the execution engine + const mockExecuteWorkflow = jest.fn().mockResolvedValue({ + id: "exec-1", + status: "completed", + results: new Map(), + }); + + // Replace the execution engine with mock + jest.doMock("../../services/executionEngine", () => ({ + executionEngine: { + executeWorkflow: mockExecuteWorkflow, + }, + })); + + await act(async () => { + await result.current.executeWorkflow("test input"); + }); + + expect(mockExecuteWorkflow).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/__tests__/utils/variableSubstitution.simple.test.ts b/src/__tests__/utils/variableSubstitution.simple.test.ts new file mode 100644 index 0000000..7076f38 --- /dev/null +++ b/src/__tests__/utils/variableSubstitution.simple.test.ts @@ -0,0 +1,41 @@ +import { substituteVariables, NodeOutput } from '../../utils/variableSubstitution'; + +describe('Variable Substitution - Simple Tests', () => { + it('should substitute basic variables', () => { + const template = 'Hello {{node1.output}}'; + const nodeOutputs = new Map([ + ['node1', { nodeId: 'node1', output: 'World', data: 'World' }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe('Hello World'); + }); + + it('should handle missing nodes gracefully', () => { + const template = 'Hello {{missingNode.output}}'; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe('Hello {{missingNode.output}}'); + }); + + it('should handle empty template', () => { + const template = ''; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe(''); + }); + + it('should handle null template', () => { + const template = null as any; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe(null); + }); +}); diff --git a/src/__tests__/utils/variableSubstitution.test.ts b/src/__tests__/utils/variableSubstitution.test.ts new file mode 100644 index 0000000..6e800b4 --- /dev/null +++ b/src/__tests__/utils/variableSubstitution.test.ts @@ -0,0 +1,295 @@ +import { + substituteVariables, + NodeOutput, +} from "../../utils/variableSubstitution"; + +describe("Variable Substitution", () => { + describe("Basic Substitution", () => { + it("should substitute simple node output", () => { + const template = "Hello {{node1.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "World", data: "World" }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Hello World"); + }); + + it("should substitute multiple variables", () => { + const template = "{{node1.output}} and {{node2.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "Hello", data: "Hello" }], + ["node2", { nodeId: "node2", output: "World", data: "World" }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Hello and World"); + }); + + it("should handle empty template", () => { + const template = ""; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe(""); + }); + + it("should handle template without variables", () => { + const template = "No variables here"; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("No variables here"); + }); + }); + + describe("Property Access", () => { + it("should access output property", () => { + const template = "{{node1.output}}"; + const nodeOutputs = new Map([ + [ + "node1", + { nodeId: "node1", output: "Test Output", data: "Test Data" }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Test Output"); + }); + + it("should access data property", () => { + const template = "{{node1.data}}"; + const nodeOutputs = new Map([ + [ + "node1", + { nodeId: "node1", output: "Test Output", data: "Test Data" }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Test Data"); + }); + + it("should access error property", () => { + const template = "{{node1.error}}"; + const nodeOutputs = new Map([ + [ + "node1", + { nodeId: "node1", output: "Test Output", error: "Test Error" }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Test Error"); + }); + + it("should access status property", () => { + const template = "{{node1.status}}"; + const nodeOutputs = new Map([ + [ + "node1", + { nodeId: "node1", output: "Test Output", status: "completed" }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("completed"); + }); + }); + + describe("Node Label to ID Mapping", () => { + it("should resolve node labels to IDs", () => { + const template = "{{inputNode.output}}"; + const nodeOutputs = new Map([ + [ + "node-1", + { nodeId: "node-1", output: "Input Data", data: "Input Data" }, + ], + ]); + const nodeLabelToId = new Map([["inputNode", "node-1"]]); + + const result = substituteVariables(template, nodeOutputs, nodeLabelToId); + + expect(result).toBe("Input Data"); + }); + + it("should fall back to direct ID when label not found", () => { + const template = "{{node-1.output}}"; + const nodeOutputs = new Map([ + [ + "node-1", + { + nodeId: "node-1", + output: "Direct ID Data", + data: "Direct ID Data", + }, + ], + ]); + const nodeLabelToId = new Map(); + + const result = substituteVariables(template, nodeOutputs, nodeLabelToId); + + expect(result).toBe("Direct ID Data"); + }); + }); + + describe("Missing Node Handling", () => { + it("should return original template when node not found", () => { + const template = "{{missingNode.output}}"; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("{{missingNode.output}}"); + }); + + it("should warn when node not found", () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + const template = "{{missingNode.output}}"; + const nodeOutputs = new Map(); + + substituteVariables(template, nodeOutputs); + + expect(consoleSpy).toHaveBeenCalledWith( + "Node missingNode (missingNode) not found for variable {{missingNode.output}}" + ); + + consoleSpy.mockRestore(); + }); + + it("should show available nodes in warning", () => { + const consoleSpy = jest.spyOn(console, "warn").mockImplementation(); + const template = "{{missingNode.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "Data1", data: "Data1" }], + ["node2", { nodeId: "node2", output: "Data2", data: "Data2" }], + ]); + + substituteVariables(template, nodeOutputs); + + expect(consoleSpy).toHaveBeenCalledWith("Available nodes: node1, node2"); + + consoleSpy.mockRestore(); + }); + }); + + describe("Complex Templates", () => { + it("should handle mixed content and variables", () => { + const template = "Process {{node1.output}} and save to {{node2.data}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "input.txt", data: "input.txt" }], + [ + "node2", + { nodeId: "node2", output: "output.txt", data: "output.txt" }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Process input.txt and save to output.txt"); + }); + + it("should handle multiple occurrences of same variable", () => { + const template = "{{node1.output}} and {{node1.output}} again"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "Hello", data: "Hello" }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Hello and Hello again"); + }); + + it("should handle nested object properties", () => { + const template = "{{node1.result.data}}"; + const nodeOutputs = new Map([ + [ + "node1", + { + nodeId: "node1", + output: { result: { data: "Nested Data" } }, + data: { result: { data: "Nested Data" } }, + }, + ], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Nested Data"); + }); + }); + + describe("Type Conversion", () => { + it("should convert numbers to strings", () => { + const template = "Count: {{node1.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: 42, data: 42 }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Count: 42"); + }); + + it("should convert booleans to strings", () => { + const template = "Status: {{node1.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: true, data: true }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Status: true"); + }); + + it("should handle null and undefined values", () => { + const template = "Value: {{node1.output}}"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: null, data: null }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("Value: "); + }); + }); + + describe("Error Handling", () => { + it("should handle malformed variable syntax", () => { + const template = "{{node1.output"; + const nodeOutputs = new Map([ + ["node1", { nodeId: "node1", output: "Data", data: "Data" }], + ]); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("{{node1.output"); + }); + + it("should handle empty variable names", () => { + const template = "{{}}"; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe("{{}}"); + }); + + it("should handle non-string templates", () => { + const template = null as any; + const nodeOutputs = new Map(); + + const result = substituteVariables(template, nodeOutputs); + + expect(result).toBe(null); + }); + }); +}); diff --git a/src/setupTests.ts b/src/setupTests.ts new file mode 100644 index 0000000..9e9da64 --- /dev/null +++ b/src/setupTests.ts @@ -0,0 +1,44 @@ +import "@testing-library/jest-dom"; + +// Mock localStorage +const localStorageMock = { + getItem: jest.fn(), + setItem: jest.fn(), + removeItem: jest.fn(), + clear: jest.fn(), +}; +Object.defineProperty(window, "localStorage", { + value: localStorageMock, +}); + +// Mock console methods to reduce noise in tests +global.console = { + ...console, + // Uncomment to ignore a specific log level + // log: jest.fn(), + // debug: jest.fn(), + // info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), +}; + +// Mock ResizeObserver +global.ResizeObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Mock IntersectionObserver +global.IntersectionObserver = jest.fn().mockImplementation(() => ({ + observe: jest.fn(), + unobserve: jest.fn(), + disconnect: jest.fn(), +})); + +// Mock crypto for UUID generation +Object.defineProperty(global, "crypto", { + value: { + randomUUID: () => "mock-uuid-" + Math.random().toString(36).substr(2, 9), + }, +});