diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 891bcd09c..96e871fdd 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -10,20 +10,21 @@ "dependencies": { "@xyflow/react": "^12.11.1", "react": "^19.2.4", - "react-dom": "^19.2.8" + "react-dom": "^19.2.7", + "undici": "^8.10.0" }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.4", + "@types/react-dom": "^19.2.3", "@vitest/coverage-v8": "^4.1.9", "fast-check": "^4.8.0", - "jsdom": "^30.0.1", + "jsdom": "^29.1.1", "typescript": "^6.0.3", - "vite": "^8.2.0", - "vitest": "^4.1.10" + "vite": "^8.1.0", + "vitest": "^4.1.9" }, "engines": { "node": ">=26 <27" @@ -37,38 +38,56 @@ "license": "MIT" }, "node_modules/@asamuzakjp/css-color": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-6.0.5.tgz", - "integrity": "sha512-mbhpPMmnw/kwW19aRNmSUl1QzLbdGo1SCuE49BT98MNwqF6zaHb3o2owssFc/PEO/4t2UjqtCNwocuDtJornzA==", + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", + "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", "dev": true, "license": "MIT", "dependencies": { - "@csstools/css-calc": "^3.2.1", - "@csstools/css-color-parser": "^4.1.9", + "@asamuzakjp/generational-cache": "^1.0.1", + "@csstools/css-calc": "^3.2.0", + "@csstools/css-color-parser": "^4.1.0", "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0", - "lru-cache": "^11.5.2" + "@csstools/css-tokenizer": "^4.0.0" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "node_modules/@asamuzakjp/dom-selector": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-8.3.2.tgz", - "integrity": "sha512-93Z1N+BQNXysodoicpOIyNh2drHfz/CTf9nnT0FEx72GJcIiwgydD7tGAr78j41LsYn3hlRn+LdGPuBLn1Bl8Q==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", + "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", "dev": true, "license": "MIT", "dependencies": { + "@asamuzakjp/generational-cache": "^1.0.1", + "@asamuzakjp/nwsapi": "^2.3.9", "bidi-js": "^1.0.3", "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2" + "is-potential-custom-element-name": "^1.0.1" }, "engines": { - "node": "^22.13.0 || >=24.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" + } + }, + "node_modules/@asamuzakjp/generational-cache": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", + "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, + "node_modules/@asamuzakjp/nwsapi": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", + "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@babel/code-frame": { "version": "7.29.7", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", @@ -177,9 +196,9 @@ } }, "node_modules/@csstools/color-helpers": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.1.0.tgz", - "integrity": "sha512-064IFJdjTfUqnjpCVpMOdbr8FLQBhinbZj6yRv2An2E41O/pLEXqfFRWqGq/SxlE5PEUYTlvWsG2r8MswAVvkg==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", + "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", "dev": true, "funding": [ { @@ -197,9 +216,9 @@ } }, "node_modules/@csstools/css-calc": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.3.0.tgz", - "integrity": "sha512-c5ihYsPkdG6JCkU2zTMm4+k6r7RXuGxtWYhu5DHMIiF1FHzrfmHL5so11AoFpUv/tu61xfcmT4AmKoFfMPoqdQ==", + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.1.tgz", + "integrity": "sha512-DtdHlgXh5ZkA43cwBcAm+huzgJiwx3ZTWVjBs94kwz2xKqSimDA3lBgCjphYgwgVUMWatSM0pDd8TILB1yrVVg==", "dev": true, "funding": [ { @@ -221,9 +240,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.10.tgz", - "integrity": "sha512-UZhQLIUyJaaMepqehrCODwCg2KW25vFvLWBmqYFaPclYvvxzj/sG8LBOhBFCp11i9uE7t1EyS+RAoV9tztPFyw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", "dev": true, "funding": [ { @@ -237,8 +256,8 @@ ], "license": "MIT", "dependencies": { - "@csstools/color-helpers": "^6.1.0", - "@csstools/css-calc": "^3.3.0" + "@csstools/color-helpers": "^6.0.2", + "@csstools/css-calc": "^3.2.1" }, "engines": { "node": ">=20.19.0" @@ -272,9 +291,9 @@ } }, "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.7.tgz", - "integrity": "sha512-fQ+05118eQS1cofO3aJpB5efgpBZMvIzwr/sbC8kDLVA5XLG8q1kJV5yzrUAI1f7lvhPnm8fgIjzFB8/O/5Dig==", + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.5.tgz", + "integrity": "sha512-oNjBvzLq2GPZtJphCjLqXow/cHySHSgtxvKZb7OqSZ/xHgw6NWNhfad+6AB9cLeVm6eA9d/qMll3JdEHjy6M+A==", "dev": true, "funding": [ { @@ -317,21 +336,21 @@ } }, "node_modules/@emnapi/core": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-2.0.0-alpha.3.tgz", - "integrity": "sha512-AZypUeJ/yByuxyS7BlSNRDOMLMlROYtjYdIAuBmJssVz1UJDSeYxLrdizhXCFYhedC5bqd/ASy8EuNXbVVXp9g==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/wasi-threads": "2.0.1", + "@emnapi/wasi-threads": "1.2.2", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "2.0.0-alpha.3", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-2.0.0-alpha.3.tgz", - "integrity": "sha512-hFPAhMUjJD9BSyCANEISPOogeXC9Zo9ZQl7L6vKnaVsMkCtzznaW/naYypeyl0Gv5rYfWYsZbpixTMpjDJzQeA==", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "dev": true, "license": "MIT", "optional": true, @@ -340,9 +359,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-2.0.1.tgz", - "integrity": "sha512-9DsSk+o5NBX0CCJT8s0EROGSGxjR/tKu6aBTaVyq+SjAEQH4XcdcRxPBRzsBLizTTJ49MJjF+jgu3qnO9GLQcQ==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", "dev": true, "license": "MIT", "optional": true, @@ -397,31 +416,28 @@ } }, "node_modules/@napi-rs/wasm-runtime": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.2.tgz", - "integrity": "sha512-JfB4kuJQjaoHuCTseIINHtHWeJnvgEcxjwA5t/Y00ZgaOO1Crz3fjT/p8kT28zA/Caz7oiUMn3d6H2yOVCVwuw==", + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", + "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", "dev": true, "license": "MIT", "optional": true, "dependencies": { "@tybys/wasm-util": "^0.10.3" }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" - }, "funding": { "type": "github", "url": "https://github.com/sponsors/Brooooooklyn" }, "peerDependencies": { - "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.3", - "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.3" + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" } }, "node_modules/@oxc-project/types": { - "version": "0.142.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.142.0.tgz", - "integrity": "sha512-7W+2q5AKQVU36fkaryontrHn3YDt1RyUYXatw9i5H8ocYe2sPKSFB6eS8WNPeRKiN1qAWWZUPm7gwFzJGrccqQ==", + "version": "0.137.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.137.0.tgz", + "integrity": "sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==", "dev": true, "license": "MIT", "funding": { @@ -429,9 +445,9 @@ } }, "node_modules/@rolldown/binding-android-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.1.tgz", - "integrity": "sha512-02hOeOSryYxVrOIphmLAsqnCJWxwlzFk+pEt/N/i6OgT3lShHO7xGCU5cpgchRDHboAEbSjzgGh+O/u1GswQmA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.3.tgz", + "integrity": "sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==", "cpu": [ "arm64" ], @@ -446,9 +462,9 @@ } }, "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.1.tgz", - "integrity": "sha512-fMsTOnN0OjFm3CyppWPitKnc8UlliVARUULW6cfU6AIqjdtgmSFWSk9vecHzZduv/yMWIHDlRhM1e8Iff9uAfA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.3.tgz", + "integrity": "sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==", "cpu": [ "arm64" ], @@ -463,9 +479,9 @@ } }, "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.1.tgz", - "integrity": "sha512-1wjKdz/XLGKHaTNHjQveQ/B23TKx4ItAqm1JbyVuvNPc4Ze0Fb48s49TAd/2zcplPl8okE/UbTgmlVfwT7eFeQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.3.tgz", + "integrity": "sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==", "cpu": [ "x64" ], @@ -480,9 +496,9 @@ } }, "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.1.tgz", - "integrity": "sha512-Fa0jHR07E7YBN4vOEsbVf2briYNsuOowfLJaXULZM0ldMlaCaj2LJgLMbMe4iacRyZmvR8efFhgR9wKuGclQUg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.3.tgz", + "integrity": "sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==", "cpu": [ "x64" ], @@ -497,9 +513,9 @@ } }, "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.1.tgz", - "integrity": "sha512-pzkgu1SSHGgRRyRZ4fbmSgmajbVt+epaLP99NDjFft69v/ypfTi6swBMiVdh2EkQ0OSnHE1lZDM7DRGkyAzUpA==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.3.tgz", + "integrity": "sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==", "cpu": [ "arm" ], @@ -514,16 +530,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.1.tgz", - "integrity": "sha512-QI5SEDY8cbiYWHx0VO4vIc3UlS6a32vXHjU8Qy/17adEmZIPuByJg13UEvo9c/UCiUkdcVWY83C+b+JrwnNyUg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.3.tgz", + "integrity": "sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -534,16 +547,13 @@ } }, "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.1.tgz", - "integrity": "sha512-Sm41FyCeXqmYcERoYOCbGIL5hNfd8w9LQ7Y61Bev48HkcjaJqV/iiVOaiDxjVTRMS+QKrZmD8cfPt4uMVnvM+A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.3.tgz", + "integrity": "sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -554,16 +564,13 @@ } }, "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.1.tgz", - "integrity": "sha512-2x+WhXTGl9yJYPbltW/BSEPTVz9OIWQyER4N+gJEDWkkn904eRcBzELqh/Hf7K0w/ubGbKNMv0ZC+94QK/IFEg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.3.tgz", + "integrity": "sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==", "cpu": [ "ppc64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -574,16 +581,13 @@ } }, "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.1.tgz", - "integrity": "sha512-eEjmQpuRQayHPWWnywaWHkFT3ToPbP3RYy42VVd/B9aBGDA+Ol25EIWHxKQST3IiWJjikCWUF7KtbfqwZrzVwQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.3.tgz", + "integrity": "sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==", "cpu": [ "s390x" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -594,16 +598,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.1.tgz", - "integrity": "sha512-/Orga1fZYkLc/56jBICcHrKchl8Z2UKdDSr3LG9ToWO1lQ6a4Livk9Xz+9WN91zsz5QR3XQz2NNoSDEvP6qadw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.3.tgz", + "integrity": "sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MIT", "optional": true, "os": [ @@ -614,16 +615,13 @@ } }, "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.1.tgz", - "integrity": "sha512-xxBJRL+0q0Kce7orznGWLuylHDY65vuARXZRpX+hPdv+DqK2c3NlCsVA98tlWzWNEE7yPqA/1NQ5nnCrj49Y5A==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.3.tgz", + "integrity": "sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MIT", "optional": true, "os": [ @@ -634,9 +632,9 @@ } }, "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.1.tgz", - "integrity": "sha512-M6AdXIXw3s+/8XpKMzdGDEXGS1S7kwUsy+rcTIUIOx5Ge4nXKCtAFHFV9YKkXvGcC5WMoTjAteLzlsQROVI0Yw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.3.tgz", + "integrity": "sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==", "cpu": [ "arm64" ], @@ -651,25 +649,28 @@ } }, "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.2.1.tgz", - "integrity": "sha512-/TX0SoRGojHzSAHpfVBbavRVSazg5U3h3Y3VXfcc0cdugq6kxdqw8LPGFiPr+/7gE/60zRcsOY2Vi9b9eT0jww==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.3.tgz", + "integrity": "sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==", + "cpu": [ + "wasm32" + ], "dev": true, "license": "MIT", "optional": true, "dependencies": { - "@emnapi/core": "2.0.0-alpha.3", - "@emnapi/runtime": "2.0.0-alpha.3", - "@napi-rs/wasm-runtime": "^1.2.0" + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.1.6" }, "engines": { - "node": "^20.19.0 || ^22.13.0 || >=23.5.0" + "node": "^20.19.0 || >=22.12.0" } }, "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.1.tgz", - "integrity": "sha512-EvRrivJieyHG+AO9lleZWgq+g0+S7oV2C51yuqlcyU/R9net+sI4Pj0F+lUoP2bEr6TWX3SqFaaS0SzfLxSzkw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.3.tgz", + "integrity": "sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==", "cpu": [ "arm64" ], @@ -684,9 +685,9 @@ } }, "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.1.tgz", - "integrity": "sha512-Z4eCmn5QJ/5+azF9knpLWKfVd9aidn0mAe9TpJgvBLId9Ax3t0+JVxBmT25Bv7NBbVW1TZyKjQjQReouMeH5UQ==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.3.tgz", + "integrity": "sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==", "cpu": [ "x64" ], @@ -908,9 +909,9 @@ } }, "node_modules/@types/react-dom": { - "version": "19.2.4", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.4.tgz", - "integrity": "sha512-Bsc+QHgp+P/F02XDzNCY9jnZNCUuLki36KT7VKrTXXLdHf+vHMNZnW1rVu5DNW/rCK+fya3DATySbLM4yhtKUw==", + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", "devOptional": true, "license": "MIT", "peerDependencies": { @@ -918,14 +919,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.10.tgz", - "integrity": "sha512-IM49HmthevbgAO4anp1hwtoT9wYe59w0LR00gr+eagHE+ZJ5lK4sLPeO0ubgoJcwLk6dehU3R24N+FbEEKDc8g==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", + "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.9", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -939,8 +940,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.10", - "vitest": "4.1.10" + "@vitest/browser": "4.1.9", + "vitest": "4.1.9" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -949,16 +950,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.10.tgz", - "integrity": "sha512-YsCn+qAk1GWjQOWFEsEcL2gNQ0zmVmQu3T03qP6UyjhtmdtwtbuI+DASn/7iQB3HGTXkdBwGddzxPlmiql5vlA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", + "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -967,13 +968,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.10.tgz", - "integrity": "sha512-v0xaezt+DKEmKfaxg133ldzADrwLGd7Ze1MfQQTYfvs8OqZIwbxyxaYURivwV7sWy5fqn3rH5uOrSp07bp44Ow==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", + "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.10", + "@vitest/spy": "4.1.9", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -994,9 +995,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.10.tgz", - "integrity": "sha512-W1HsjSH4MXQ9YfmmhLAoIYf1HRfekQCGngeIgcei6MP5QQGWUe0gkopdZQaVCFO+JDJMrAJGwa5pRpNpvy4P8Q==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", + "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", "dev": true, "license": "MIT", "dependencies": { @@ -1007,13 +1008,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.10.tgz", - "integrity": "sha512-IKI6kpIH+LmpROplyLwBBaCfMgOZOMsygVa6BARD6ahA04VRuJSa6OaVG7kRvSEMD870Vd91rSSw0eegtWyLGg==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", + "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.10", + "@vitest/utils": "4.1.9", "pathe": "^2.0.3" }, "funding": { @@ -1021,14 +1022,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.10.tgz", - "integrity": "sha512-xRkfOT1qpTAi/Ti4Y1LtfRc3kEuqxGw59eN2jN9pRWMtS/XDevekhcFSqvQqjUNGksfjMJu3Y+oJ+4Ypn2OaJw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", + "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/pretty-format": "4.1.9", + "@vitest/utils": "4.1.9", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -1037,9 +1038,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.10.tgz", - "integrity": "sha512-PLf/Ugvoq5wO/b4rwYCR1h2PSIdXz7wnkQFMiUpLdtM7l6pqVFcQIBEHyT1+l+cj7mNwAfZHzqXqDyjvOuwbDw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", + "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", "dev": true, "license": "MIT", "funding": { @@ -1047,13 +1048,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.10.tgz", - "integrity": "sha512-fy9am/HWxbaGt/Sawrp90vt6Y6jQwf1RX77cz3uwoJwJVMli/e1IEwRPnMNJ7vKfPTwo0diXifkpPvwH9v7nGA==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", + "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.10", + "@vitest/pretty-format": "4.1.9", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -1565,39 +1566,39 @@ "license": "MIT" }, "node_modules/jsdom": { - "version": "30.0.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-30.0.1.tgz", - "integrity": "sha512-52v7mUVUfNQVYYqE1lcdaymWL0njO7lTLUog6ZvW2U5KsbiLk/GnZlVJ+qx0xfNJZ6Gn+KSpPNE52vurbxZwrA==", + "version": "29.1.1", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", + "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", "dev": true, "license": "MIT", "dependencies": { - "@asamuzakjp/css-color": "^6.0.5", - "@asamuzakjp/dom-selector": "^8.3.0", + "@asamuzakjp/css-color": "^5.1.11", + "@asamuzakjp/dom-selector": "^7.1.1", "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.7", - "@exodus/bytes": "^1.15.1", + "@csstools/css-syntax-patches-for-csstree": "^1.1.3", + "@exodus/bytes": "^1.15.0", "css-tree": "^3.2.1", "data-urls": "^7.0.0", "decimal.js": "^10.6.0", "html-encoding-sniffer": "^6.0.0", "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.5.2", + "lru-cache": "^11.3.5", "parse5": "^8.0.1", "saxes": "^6.0.0", "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.2", - "undici": "^8.9.0", + "tough-cookie": "^6.0.1", + "undici": "^7.25.0", "w3c-xmlserializer": "^5.0.0", "webidl-conversions": "^8.0.1", "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^17.1.0", + "whatwg-url": "^16.0.1", "xml-name-validator": "^5.0.0" }, "engines": { - "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + "node": "^20.19.0 || ^22.13.0 || >=24.0.0" }, "peerDependencies": { - "canvas": "^3.2.3" + "canvas": "^3.0.0" }, "peerDependenciesMeta": { "canvas": { @@ -1605,25 +1606,20 @@ } } }, - "node_modules/jsdom/node_modules/whatwg-url": { - "version": "17.1.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-17.1.0.tgz", - "integrity": "sha512-3GeworPmc2ZfEEHP7lEbUfBX/L75wdEsi0rLNhXcXxnoN5jyq0SL5gCy06SGW2cyTIZdTvWIDQNQoza++vKeaw==", + "node_modules/jsdom/node_modules/undici": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", + "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", "dev": true, "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.15.1", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, "engines": { - "node": "^22.14.0 || >=24.0.0" + "node": ">=20.18.1" } }, "node_modules/lightningcss": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", - "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "dev": true, "license": "MPL-2.0", "dependencies": { @@ -1637,23 +1633,23 @@ "url": "https://opencollective.com/parcel" }, "optionalDependencies": { - "lightningcss-android-arm64": "1.33.0", - "lightningcss-darwin-arm64": "1.33.0", - "lightningcss-darwin-x64": "1.33.0", - "lightningcss-freebsd-x64": "1.33.0", - "lightningcss-linux-arm-gnueabihf": "1.33.0", - "lightningcss-linux-arm64-gnu": "1.33.0", - "lightningcss-linux-arm64-musl": "1.33.0", - "lightningcss-linux-x64-gnu": "1.33.0", - "lightningcss-linux-x64-musl": "1.33.0", - "lightningcss-win32-arm64-msvc": "1.33.0", - "lightningcss-win32-x64-msvc": "1.33.0" + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" } }, "node_modules/lightningcss-android-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", - "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", "cpu": [ "arm64" ], @@ -1672,9 +1668,9 @@ } }, "node_modules/lightningcss-darwin-arm64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", - "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", "cpu": [ "arm64" ], @@ -1693,9 +1689,9 @@ } }, "node_modules/lightningcss-darwin-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", - "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", "cpu": [ "x64" ], @@ -1714,9 +1710,9 @@ } }, "node_modules/lightningcss-freebsd-x64": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", - "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", "cpu": [ "x64" ], @@ -1735,9 +1731,9 @@ } }, "node_modules/lightningcss-linux-arm-gnueabihf": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", - "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", "cpu": [ "arm" ], @@ -1756,16 +1752,13 @@ } }, "node_modules/lightningcss-linux-arm64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", - "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1780,16 +1773,13 @@ } }, "node_modules/lightningcss-linux-arm64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", - "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", "cpu": [ "arm64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1804,16 +1794,13 @@ } }, "node_modules/lightningcss-linux-x64-gnu": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", - "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "glibc" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1828,16 +1815,13 @@ } }, "node_modules/lightningcss-linux-x64-musl": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", - "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", "cpu": [ "x64" ], "dev": true, - "libc": [ - "musl" - ], "license": "MPL-2.0", "optional": true, "os": [ @@ -1852,9 +1836,9 @@ } }, "node_modules/lightningcss-win32-arm64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", - "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", "cpu": [ "arm64" ], @@ -1873,9 +1857,9 @@ } }, "node_modules/lightningcss-win32-x64-msvc": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", - "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", "cpu": [ "x64" ], @@ -1894,9 +1878,9 @@ } }, "node_modules/lru-cache": { - "version": "11.5.2", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz", - "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==", + "version": "11.5.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.1.tgz", + "integrity": "sha512-RPimw/7aMdv2oqRrxKwvZXcPfwBrn/JZ2xYcY9Hus/6LaS3VOAKVWKWgNLCFSiOm1ESXinjsDlidVU7JlnCN2A==", "dev": true, "license": "BlueOak-1.0.0", "engines": { @@ -2030,9 +2014,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz", - "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==", + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", "dev": true, "license": "MIT", "engines": { @@ -2115,24 +2099,24 @@ "license": "MIT" }, "node_modules/react": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", - "integrity": "sha512-PWaYA1L/q9u2u7xYQi+Y3L3Yfnie7XyLeaJICV1MGD6LprsBxcAqGjYyr0eY3p+QdsA+x/Irkt4Qif8D63+Sbw==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.7.tgz", + "integrity": "sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==", "license": "MIT", "engines": { "node": ">=0.10.0" } }, "node_modules/react-dom": { - "version": "19.2.8", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.8.tgz", - "integrity": "sha512-rVprimfGBG3DR+Tq0IQG2DT5PxKth1WIGDmj5yPmlzr4YBe7uyE+Du4oVqTDXZSHGGGXRtTJEGSSePyQCMBglQ==", + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.7.tgz", + "integrity": "sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==", "license": "MIT", "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { - "react": "^19.2.8" + "react": "^19.2.7" } }, "node_modules/react-is": { @@ -2168,13 +2152,13 @@ } }, "node_modules/rolldown": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.1.tgz", - "integrity": "sha512-4FKJhg8d3OiyQOA6Q1Q0hoFFpW9/OoX+VsHzpECsdsIZoOArrAK90gl59YK/Z+gnDel45bgJZK03ozH/9bCqEw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.3.tgz", + "integrity": "sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.142.0", + "@oxc-project/types": "=0.137.0", "@rolldown/pluginutils": "^1.0.0" }, "bin": { @@ -2184,21 +2168,21 @@ "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.2.1", - "@rolldown/binding-darwin-arm64": "1.2.1", - "@rolldown/binding-darwin-x64": "1.2.1", - "@rolldown/binding-freebsd-x64": "1.2.1", - "@rolldown/binding-linux-arm-gnueabihf": "1.2.1", - "@rolldown/binding-linux-arm64-gnu": "1.2.1", - "@rolldown/binding-linux-arm64-musl": "1.2.1", - "@rolldown/binding-linux-ppc64-gnu": "1.2.1", - "@rolldown/binding-linux-s390x-gnu": "1.2.1", - "@rolldown/binding-linux-x64-gnu": "1.2.1", - "@rolldown/binding-linux-x64-musl": "1.2.1", - "@rolldown/binding-openharmony-arm64": "1.2.1", - "@rolldown/binding-wasm32-wasi": "1.2.1", - "@rolldown/binding-win32-arm64-msvc": "1.2.1", - "@rolldown/binding-win32-x64-msvc": "1.2.1" + "@rolldown/binding-android-arm64": "1.1.3", + "@rolldown/binding-darwin-arm64": "1.1.3", + "@rolldown/binding-darwin-x64": "1.1.3", + "@rolldown/binding-freebsd-x64": "1.1.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.1.3", + "@rolldown/binding-linux-arm64-gnu": "1.1.3", + "@rolldown/binding-linux-arm64-musl": "1.1.3", + "@rolldown/binding-linux-ppc64-gnu": "1.1.3", + "@rolldown/binding-linux-s390x-gnu": "1.1.3", + "@rolldown/binding-linux-x64-gnu": "1.1.3", + "@rolldown/binding-linux-x64-musl": "1.1.3", + "@rolldown/binding-openharmony-arm64": "1.1.3", + "@rolldown/binding-wasm32-wasi": "1.1.3", + "@rolldown/binding-win32-arm64-msvc": "1.1.3", + "@rolldown/binding-win32-x64-msvc": "1.1.3" } }, "node_modules/saxes": { @@ -2342,29 +2326,29 @@ } }, "node_modules/tldts": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.10.tgz", - "integrity": "sha512-GgouD1B+sWwvkaEq8vXC15DjQitxbvs12oIXELpconwm+Tg3zfcEv4jgzq3vtKverDXsg3VI8aRgNL2Nra0Iog==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-7.4.3.tgz", + "integrity": "sha512-A3BDQBeeukYPzB4QdQ1DtdlUmp4x2OCH8n5UVhEWbyANxNep8GavottKzd1xYKFJKjUgMyPT7EzOfnBO55s8Sg==", "dev": true, "license": "MIT", "dependencies": { - "tldts-core": "^7.4.10" + "tldts-core": "^7.4.3" }, "bin": { "tldts": "bin/cli.js" } }, "node_modules/tldts-core": { - "version": "7.4.10", - "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.10.tgz", - "integrity": "sha512-KnQjp53ZekKgm/r3l+u8kJGGzYgrWdP8+Mql7a4vijh2WE0IrZWspQj/TpTxDho/YxO+AnOZnIjQcCD+q6iJsw==", + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.3.tgz", + "integrity": "sha512-27ep5H9PzdBrNd5OFM/j3WCU8F3kPwM9D0BOaOf7uYfxMJfyr0K5Tjj69Gri+sZlh2WXd5buIm47NuPF29CDiw==", "dev": true, "license": "MIT" }, "node_modules/tough-cookie": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.2.tgz", - "integrity": "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-6.0.1.tgz", + "integrity": "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -2413,7 +2397,6 @@ "version": "8.10.0", "resolved": "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz", "integrity": "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==", - "dev": true, "license": "MIT", "engines": { "node": ">=22.19.0" @@ -2429,16 +2412,16 @@ } }, "node_modules/vite": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.0.tgz", - "integrity": "sha512-pn+CFpM0lwDeKwmOq1ZaBK/9sjorZcgqxki6MbY/jPEVd9vichIlmlD4HmQ5wdP5EgqQCFRaACBxMC7uEGc6lQ==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.0.tgz", + "integrity": "sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.33.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.23", - "rolldown": "~1.2.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "~1.1.2", "tinyglobby": "^0.2.17" }, "bin": { @@ -2455,7 +2438,7 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.4.0", + "@vitejs/devtools": "^0.3.0", "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", @@ -2507,19 +2490,19 @@ } }, "node_modules/vitest": { - "version": "4.1.10", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.10.tgz", - "integrity": "sha512-R9jUTe5S4Qb0HCd4TNqpC7oGcrMssMRGXLW80ubjWsW9VH5GF8y1Y0SFLY9AbqSk6nt0PnOx4H4WNJYZ13GUPw==", + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", + "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.10", - "@vitest/mocker": "4.1.10", - "@vitest/pretty-format": "4.1.10", - "@vitest/runner": "4.1.10", - "@vitest/snapshot": "4.1.10", - "@vitest/spy": "4.1.10", - "@vitest/utils": "4.1.10", + "@vitest/expect": "4.1.9", + "@vitest/mocker": "4.1.9", + "@vitest/pretty-format": "4.1.9", + "@vitest/runner": "4.1.9", + "@vitest/snapshot": "4.1.9", + "@vitest/spy": "4.1.9", + "@vitest/utils": "4.1.9", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -2547,12 +2530,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.10", - "@vitest/browser-preview": "4.1.10", - "@vitest/browser-webdriverio": "4.1.10", - "@vitest/coverage-istanbul": "4.1.10", - "@vitest/coverage-v8": "4.1.10", - "@vitest/ui": "4.1.10", + "@vitest/browser-playwright": "4.1.9", + "@vitest/browser-preview": "4.1.9", + "@vitest/browser-webdriverio": "4.1.9", + "@vitest/coverage-istanbul": "4.1.9", + "@vitest/coverage-v8": "4.1.9", + "@vitest/ui": "4.1.9", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" diff --git a/frontend/package.json b/frontend/package.json index f179f4570..ba14cd262 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -19,20 +19,21 @@ "dependencies": { "@xyflow/react": "^12.11.1", "react": "^19.2.4", - "react-dom": "^19.2.8" + "react-dom": "^19.2.7" }, "devDependencies": { "@testing-library/jest-dom": "^6.9.1", "@testing-library/react": "^16.3.2", "@testing-library/user-event": "^14.6.1", "@types/react": "^19.2.10", - "@types/react-dom": "^19.2.4", + "@types/react-dom": "^19.2.3", "@vitest/coverage-v8": "^4.1.9", "fast-check": "^4.8.0", - "jsdom": "^30.0.1", + "jsdom": "^29.1.1", "typescript": "^6.0.3", - "vite": "^8.2.0", - "vitest": "^4.1.10" + "vite": "^8.1.0", + "vitest": "^4.1.9", + "undici": "^8.10.0" }, "overrides": { "esbuild": "^0.25.0", diff --git a/frontend/src/App.coverage.test.tsx b/frontend/src/App.coverage.test.tsx index 0b9a20aa8..aa170db31 100644 --- a/frontend/src/App.coverage.test.tsx +++ b/frontend/src/App.coverage.test.tsx @@ -1,6 +1,13 @@ -import '@testing-library/jest-dom/vitest' -import { act, cleanup, fireEvent, render, screen, waitFor } from '@testing-library/react' -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import "@testing-library/jest-dom/vitest"; +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, +} from "@testing-library/react"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; const api = vi.hoisted(() => ({ getMe: vi.fn(), @@ -12,71 +19,112 @@ const api = vi.hoisted(() => ({ createSnapshot: vi.fn(), getSnapshot: vi.fn(), createShareLink: vi.fn(), -})) +})); const exports = vi.hoisted(() => ({ downloadText: vi.fn(), - exportDDL: vi.fn(() => 'DDL'), - exportDiagramSvg: vi.fn(() => ''), - exportDictionaryCsv: vi.fn(() => 'csv'), - exportDictionaryMarkdown: vi.fn(() => 'markdown'), - exportPlantUml: vi.fn(() => '@startuml'), - exportMermaid: vi.fn(() => 'graph TD'), - exportDbml: vi.fn(() => 'Table users {}'), + exportDDL: vi.fn(() => "DDL"), + exportDiagramSvg: vi.fn(() => ""), + exportDictionaryCsv: vi.fn(() => "csv"), + exportDictionaryMarkdown: vi.fn(() => "markdown"), + exportPlantUml: vi.fn(() => "@startuml"), + exportMermaid: vi.fn(() => "graph TD"), + exportDbml: vi.fn(() => "Table users {}"), inferRelationships: vi.fn(), -})) +})); -vi.mock('./api', () => api) -vi.mock('./erd/export', () => ({ +vi.mock("./api", () => api); +vi.mock("./erd/export", () => ({ downloadText: exports.downloadText, exportDDL: exports.exportDDL, exportDiagramSvg: exports.exportDiagramSvg, exportDictionaryCsv: exports.exportDictionaryCsv, exportDictionaryMarkdown: exports.exportDictionaryMarkdown, exportPlantUml: exports.exportPlantUml, -})) -vi.mock('./erd/mermaid', () => ({ exportMermaid: exports.exportMermaid })) -vi.mock('./erd/dbml', () => ({ exportDbml: exports.exportDbml })) -vi.mock('./erd/autoInfer', () => ({ inferRelationships: exports.inferRelationships })) +})); +vi.mock("./erd/mermaid", () => ({ exportMermaid: exports.exportMermaid })); +vi.mock("./erd/dbml", () => ({ exportDbml: exports.exportDbml })); +vi.mock("./erd/autoInfer", () => ({ + inferRelationships: exports.inferRelationships, +})); -vi.mock('@xyflow/react', async () => { - const React = await import('react') +vi.mock("@xyflow/react", async () => { + const React = await import("react"); const initialNode = { - id: 'table-1', - type: 'tableNode', + id: "table-1", + type: "tableNode", position: { x: 5, y: 10 }, data: { - title: 'public.users', + title: "public.users", columns: [ - { column_name: 'id', data_type: 'bigint', is_not_null: true, is_pk: true }, - { column_name: 'email', data_type: 'text', is_not_null: false, is_pk: false }, + { + column_name: "id", + data_type: "bigint", + is_not_null: true, + is_pk: true, + }, + { + column_name: "email", + data_type: "text", + is_not_null: false, + is_pk: false, + }, ], badges: { pk: true, fk: false }, }, - } + }; const otherNode = { ...initialNode, - id: 'table-2', + id: "table-2", position: { x: 50, y: 100 }, - data: { ...initialNode.data, title: 'public.orders' }, - } - const edge = { id: 'edge-1', source: 'table-1', target: 'table-2', label: 'fk_old' } + data: { ...initialNode.data, title: "public.orders" }, + }; + const edge = { + id: "edge-1", + source: "table-1", + target: "table-2", + label: "fk_old", + }; function ReactFlowMock(props: Record) { React.useEffect(() => { - props.onInit?.({ fitView: vi.fn() }) - }, [props.onInit]) + props.onInit?.({ fitView: vi.fn() }); + }, [props.onInit]); return (
{props.nodes.length} {props.edges.length} -
- ) + ); } return { @@ -84,55 +132,97 @@ vi.mock('@xyflow/react', async () => { Controls: () => , MiniMap: () => , Handle: () => , - Position: { Top: 'top', Left: 'left', Right: 'right', Bottom: 'bottom' }, + Position: { Top: "top", Left: "left", Right: "right", Bottom: "bottom" }, ReactFlow: ReactFlowMock, - ReactFlowProvider: ({ children }: { children: React.ReactNode }) => <>{children}, + ReactFlowProvider: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), addEdge: (next: unknown, current: unknown[]) => [...current, next], useNodesState: (initial: unknown[]) => { - const [value, setValue] = React.useState(initial) - return [value, setValue, vi.fn()] + const [value, setValue] = React.useState(initial); + return [value, setValue, vi.fn()]; }, useEdgesState: (initial: unknown[]) => { - const [value, setValue] = React.useState(initial) - return [value, setValue, vi.fn()] + const [value, setValue] = React.useState(initial); + return [value, setValue, vi.fn()]; }, __fixtures: { initialNode, otherNode, edge }, - } -}) + }; +}); -vi.mock('./erd/convert', async () => { - const flow = (await import('@xyflow/react')) as any +vi.mock("./erd/convert", async () => { + const flow = (await import("@xyflow/react")) as any; return { snapshotToGraph: vi.fn(() => ({ nodes: [flow.__fixtures.initialNode, flow.__fixtures.otherNode], edges: [flow.__fixtures.edge], })), - } -}) + }; +}); -vi.mock('./components/modals', () => ({ +vi.mock("./components/modals", () => ({ AddTableModal: (props: any) => (
-
), EditEdgeModal: (props: any) => (
-
@@ -141,87 +231,267 @@ vi.mock('./components/modals', () => ({
{props.shareLinkUrl} {props.shareLinkError} -
), GroupModal: (props: any) => (
-
), CardinalityModal: (props: any) => { const recommendation = { - index_name: 'idx_users_email', - columns: ['email'], - access_method: 'btree', + index_name: "idx_users_email", + columns: ["email"], + access_method: "btree", estimated_distinct: 50, cardinality_ratio: 0.5, - strength: 'recommended', - reason: 'selective', - source: 'cardinality-wizard', - } + strength: "recommended", + reason: "selective", + source: "cardinality-wizard", + }; return (
{props.formatPercent(0.5)} - {props.strengthLabel('recommended')} - {props.strengthLabel('consider')} - {props.strengthLabel('skip')} -
- ) + ); }, EditTableModal: (props: any) => (
- -
+
-
), -})) +})); -import App, { DiagramTable } from './App' -import { snapshotToGraph } from './erd/convert' +import App, { DiagramTable } from "./App"; +import { snapshotToGraph } from "./erd/convert"; const projects = [ - { project_space_uuid: 'p1', project_name: '' }, - { project_space_uuid: 'p2', project_name: 'HR' }, -] -const connections = [{ db_connection_uuid: 'c1', conn_name: 'Warehouse' }] + { project_space_uuid: "p1", project_name: "" }, + { project_space_uuid: "p2", project_name: "HR" }, +]; +const connections = [{ db_connection_uuid: "c1", conn_name: "Warehouse" }]; const snapshots = [ - { schema_snapshot_uuid: 's1', status: 'succeeded', schema_filter: 'billing' }, - { schema_snapshot_uuid: 's2', status: 'failed', schema_filter: null }, -] + { schema_snapshot_uuid: "s1", status: "succeeded", schema_filter: "billing" }, + { schema_snapshot_uuid: "s2", status: "failed", schema_filter: null }, +]; beforeEach(() => { - vi.clearAllMocks() - api.getMe.mockResolvedValue({ subject: 'user', display_name: 'User', user_account_uuid: 'u' }) - api.listProjects.mockResolvedValue(projects) - api.listConnections.mockResolvedValue(connections) - api.listSnapshots.mockResolvedValue(snapshots) - api.createProject.mockResolvedValue({ project_space_uuid: 'p3', project_name: 'New' }) - api.createConnection.mockResolvedValue({ db_connection_uuid: 'c2', conn_name: 'New DB' }) - api.createSnapshot.mockResolvedValue({ schema_snapshot_uuid: 's3', status: 'queued', schema_filter: 'public' }) + vi.clearAllMocks(); + api.getMe.mockResolvedValue({ + subject: "user", + display_name: "User", + user_account_uuid: "u", + }); + api.listProjects.mockResolvedValue(projects); + api.listConnections.mockResolvedValue(connections); + api.listSnapshots.mockResolvedValue(snapshots); + api.createProject.mockResolvedValue({ + project_space_uuid: "p3", + project_name: "New", + }); + api.createConnection.mockResolvedValue({ + db_connection_uuid: "c2", + conn_name: "New DB", + }); + api.createSnapshot.mockResolvedValue({ + schema_snapshot_uuid: "s3", + status: "queued", + schema_filter: "public", + }); api.getSnapshot.mockResolvedValue({ - schema_snapshot_uuid: 's3', - status: 'succeeded', - schema_filter: 'public', + schema_snapshot_uuid: "s3", + status: "succeeded", + schema_filter: "public", error_message: null, snapshot_json: { relations: [], columns: [], pk_columns: [], fk_edges: [] }, - }) - api.createShareLink.mockResolvedValue({ url: 'http://localhost/api/share/one' }) + }); + api.createShareLink.mockResolvedValue({ + url: "http://localhost/api/share/one", + }); exports.inferRelationships.mockReturnValue([ - { id: 'inferred', source: 'table-1', target: 'table-2', label: 'fk_inferred' }, - ]) - vi.stubGlobal('ResizeObserver', class { observe() {} unobserve() {} disconnect() {} }) - vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { - callback(0) - return 1 - }) - Object.defineProperty(navigator, 'clipboard', { + { + id: "inferred", + source: "table-1", + target: "table-2", + label: "fk_inferred", + }, + ]); + vi.stubGlobal( + "ResizeObserver", + class { + observe() {} + unobserve() {} + disconnect() {} + }, + ); + vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { + callback(0); + return 1; + }); + Object.defineProperty(navigator, "clipboard", { configurable: true, value: { writeText: vi.fn().mockResolvedValue(undefined) }, - }) -}) + }); +}); afterEach(() => { - cleanup() - vi.useRealTimers() - vi.unstubAllGlobals() - vi.restoreAllMocks() -}) + cleanup(); + vi.useRealTimers(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); async function renderReadyApp() { - render() - await screen.findByRole('heading', { name: '대시보드' }) + render(); + await screen.findByRole("heading", { name: "대시보드" }); } function forceClick(button: HTMLButtonElement) { - button.disabled = false - button.removeAttribute('disabled') - button.dispatchEvent(new MouseEvent('click', { bubbles: true, cancelable: true })) + button.disabled = false; + button.removeAttribute("disabled"); + button.dispatchEvent( + new MouseEvent("click", { bubbles: true, cancelable: true }), + ); } -describe('App orchestration coverage', () => { - it('shows loading and explicit authentication failure', async () => { - let rejectMe!: (reason?: unknown) => void - api.getMe.mockReturnValueOnce(new Promise((_resolve, reject) => { rejectMe = reject })) - render() - expect(screen.getByText('Authenticating…')).toBeInTheDocument() - await act(async () => rejectMe(new Error('denied'))) - expect(await screen.findByRole('heading', { name: 'Authentication required' })).toBeInTheDocument() - expect(screen.getByRole('alert')).toHaveTextContent('denied') - }) - - it('navigates dashboard, project, and diagram states including empty/search branches', async () => { - await renderReadyApp() - expect(screen.getAllByText('<Billing & Core>').length).toBeGreaterThan(0) - fireEvent.click(screen.getByRole('button', { name: '전체 보기' })) - expect(screen.getByRole('heading', { name: '프로젝트' })).toBeInTheDocument() - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[1]!) - expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument() - fireEvent.change(screen.getByLabelText('다이어그램 검색'), { target: { value: 'no-match' } }) - expect(screen.getByText('검색 결과가 없습니다.')).toBeInTheDocument() - fireEvent.change(screen.getByLabelText('다이어그램 검색'), { target: { value: 'failed' } }) - expect(screen.getByText('ERD_all_2')).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '편집기 열기' })) - expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument() - - cleanup() - api.listProjects.mockResolvedValueOnce([]) - await act(async () => render()) - await screen.findByText('아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.') - fireEvent.click(screen.getByRole('button', { name: '전체 보기' })) - expect(screen.getByText('프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.')).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - expect(screen.getByText('프로젝트를 선택하세요.')).toBeInTheDocument() - }) - - it('creates projects, validates and creates connections, and starts a snapshot', async () => { - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - - fireEvent.change(screen.getByLabelText('New project'), { target: { value: ' New ' } }) - fireEvent.click(screen.getByRole('button', { name: 'Create' })) - await waitFor(() => expect(api.createProject).toHaveBeenCalledWith('New')) - - const dsn = screen.getByLabelText('Connection DSN') - fireEvent.change(dsn, { target: { value: 'postgresql://[' } }) - fireEvent.click(screen.getByRole('button', { name: 'Save connection' })) - expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use') - fireEvent.change(dsn, { target: { value: 'http://bad.example/db' } }) - fireEvent.click(screen.getByRole('button', { name: 'Save connection' })) - expect(screen.getByRole('alert')).toHaveTextContent('Connection DSN must use') - expect(dsn).toHaveValue('') - - fireEvent.change(dsn, { target: { value: 'postgresql://db.example/test' } }) - fireEvent.click(screen.getByRole('button', { name: 'Save connection' })) - await waitFor(() => expect(api.createConnection).toHaveBeenCalledWith('p3', 'target-db', 'postgresql://db.example/test')) - - fireEvent.change(screen.getByLabelText('Schema filter (optional)'), { target: { value: ' public ' } }) - fireEvent.click(screen.getByRole('button', { name: 'Reverse engineer → snapshot' })) - await waitFor(() => expect(api.createSnapshot).toHaveBeenCalledWith('p3', 'c2', 'public')) - expect(screen.getByText('스냅샷 생성 중...')).toBeInTheDocument() - }) - - it('polls a terminal snapshot, builds graph state, and exercises editor handlers', async () => { - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - const openButtons = await screen.findAllByRole('button', { name: '열기' }) - vi.useFakeTimers() - fireEvent.click(openButtons[0]!) +describe("App orchestration coverage", () => { + it("shows loading and explicit authentication failure", async () => { + let rejectMe!: (reason?: unknown) => void; + api.getMe.mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectMe = reject; + }), + ); + render(); + expect(screen.getByText("Authenticating…")).toBeInTheDocument(); + await act(async () => rejectMe(new Error("denied"))); + expect( + await screen.findByRole("heading", { name: "Authentication required" }), + ).toBeInTheDocument(); + expect(screen.getByRole("alert")).toHaveTextContent("denied"); + }); + + it("navigates dashboard, project, and diagram states including empty/search branches", async () => { + await renderReadyApp(); + expect( + screen.getAllByText("<Billing & Core>").length, + ).toBeGreaterThan(0); + fireEvent.click(screen.getByRole("button", { name: "전체 보기" })); + expect( + screen.getByRole("heading", { name: "프로젝트" }), + ).toBeInTheDocument(); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[1]!); + expect( + screen.getByRole("heading", { name: "다이어그램" }), + ).toBeInTheDocument(); + fireEvent.change(screen.getByLabelText("다이어그램 검색"), { + target: { value: "no-match" }, + }); + expect(screen.getByText("검색 결과가 없습니다.")).toBeInTheDocument(); + fireEvent.change(screen.getByLabelText("다이어그램 검색"), { + target: { value: "failed" }, + }); + expect(screen.getByText("ERD_all_2")).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "편집기 열기" })); + expect( + screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }), + ).toBeInTheDocument(); + + cleanup(); + api.listProjects.mockResolvedValueOnce([]); + await act(async () => render()); + await screen.findByText( + "아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.", + ); + fireEvent.click(screen.getByRole("button", { name: "전체 보기" })); + expect( + screen.getByText( + "프로젝트가 없습니다. 이름을 입력해 새 프로젝트를 만드세요.", + ), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + expect(screen.getByText("프로젝트를 선택하세요.")).toBeInTheDocument(); + }); + + it("creates projects, validates and creates connections, and starts a snapshot", async () => { + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + + fireEvent.change(screen.getByLabelText("New project"), { + target: { value: " New " }, + }); + fireEvent.click(screen.getByRole("button", { name: "Create" })); + await waitFor(() => expect(api.createProject).toHaveBeenCalledWith("New")); + + const dsn = screen.getByLabelText("Connection DSN"); + fireEvent.change(dsn, { target: { value: "postgresql://[" } }); + fireEvent.click(screen.getByRole("button", { name: "Save connection" })); + expect(screen.getByRole("alert")).toHaveTextContent( + "Connection DSN must use", + ); + fireEvent.change(dsn, { target: { value: "http://bad.example/db" } }); + fireEvent.click(screen.getByRole("button", { name: "Save connection" })); + expect(screen.getByRole("alert")).toHaveTextContent( + "Connection DSN must use", + ); + expect(dsn).toHaveValue(""); + + fireEvent.change(dsn, { + target: { value: "postgresql://db.example/test" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Save connection" })); + await waitFor(() => + expect(api.createConnection).toHaveBeenCalledWith( + "p3", + "target-db", + "postgresql://db.example/test", + ), + ); + + fireEvent.change(screen.getByLabelText("Schema filter (optional)"), { + target: { value: " public " }, + }); + fireEvent.click( + screen.getByRole("button", { name: "Reverse engineer → snapshot" }), + ); + await waitFor(() => + expect(api.createSnapshot).toHaveBeenCalledWith("p3", "c2", "public"), + ); + expect(screen.getByText("스냅샷 생성 중...")).toBeInTheDocument(); + }); + + it("polls a terminal snapshot, builds graph state, and exercises editor handlers", async () => { + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + const openButtons = await screen.findAllByRole("button", { name: "열기" }); + vi.useFakeTimers(); + fireEvent.click(openButtons[0]!); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - expect(api.getSnapshot).toHaveBeenCalledWith('s1') - expect(screen.getByTestId('node-count')).toHaveTextContent('2') - - fireEvent.change(screen.getByLabelText('테이블 또는 컬럼 검색'), { target: { value: 'users' } }) - expect(screen.getByText('1개 테이블 일치', { exact: false })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' })) + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(api.getSnapshot).toHaveBeenCalledWith("s1"); + expect(screen.getByTestId("node-count")).toHaveTextContent("2"); + + const canvasSearchInput = screen.getByLabelText("테이블 또는 컬럼 검색"); + fireEvent.change(canvasSearchInput, { target: { value: "users" } }); + const canvasSearchForm = canvasSearchInput.closest("form"); + const canvasSubmitEvent = new Event("submit", { + cancelable: true, + bubbles: true, + }); + if (canvasSearchForm) canvasSearchForm.dispatchEvent(canvasSubmitEvent); + expect(canvasSubmitEvent.defaultPrevented).toBe(true); + expect( + screen.getByText("1개 테이블 일치", { exact: false }), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" })); await act(async () => { - vi.runOnlyPendingTimers() - await Promise.resolve() - }) - expect(screen.getByText('정렬 완료', { exact: false })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '정렬 되돌리기' })) - expect(screen.getByText('되돌렸습니다', { exact: false })).toBeInTheDocument() - - fireEvent.click(screen.getByTestId('flow-connect')) - fireEvent.click(screen.getByTestId('edge-label')) - fireEvent.click(screen.getByTestId('edge-submit')) - fireEvent.click(screen.getByTestId('flow-edge')) - fireEvent.click(screen.getByTestId('edge-cancel')) - fireEvent.click(screen.getByTestId('flow-edge-unlabeled')) - fireEvent.click(screen.getByTestId('edge-cancel')) - fireEvent.click(screen.getByTestId('flow-edge')) - vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true) - fireEvent.click(screen.getByTestId('edge-delete')) - fireEvent.click(screen.getByTestId('edge-delete')) - - fireEvent.doubleClick(screen.getByTestId('flow-node')) - fireEvent.submit(screen.getByTestId('table-empty-form')) - fireEvent.click(screen.getByTestId('table-cancel')) - fireEvent.doubleClick(screen.getByTestId('flow-node')) - fireEvent.submit(screen.getByTestId('table-form')) - fireEvent.doubleClick(screen.getByTestId('flow-node')) - fireEvent.click(screen.getByTestId('table-cancel')) - fireEvent.doubleClick(screen.getByTestId('flow-node')) - vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true) - fireEvent.click(screen.getByTestId('table-delete')) - fireEvent.click(screen.getByTestId('table-delete')) - }) - - it('adds nodes and exercises groups, cardinality, exports, inference, and clearing', async () => { - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!) - fireEvent.click(screen.getByTestId('add-name')) - fireEvent.click(screen.getByTestId('add-submit')) - expect(screen.getByTestId('node-count')).toHaveTextContent('1') - - fireEvent.click(screen.getByRole('button', { name: '업무 그룹' })) - fireEvent.click(screen.getByTestId('group-create-guard')) - fireEvent.click(screen.getByTestId('group-name')) - fireEvent.click(screen.getByTestId('group-create')) - fireEvent.click(screen.getByTestId('group-assign-missing')) - fireEvent.click(screen.getByTestId('group-assign')) - vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true) - fireEvent.click(screen.getByTestId('group-delete')) - fireEvent.click(screen.getByTestId('group-delete')) - fireEvent.click(screen.getByTestId('group-close')) - - fireEvent.click(screen.getByRole('button', { name: '인덱스 카디널리티 계산' })) - expect(screen.getByTestId('card-format')).toHaveTextContent('50%') - expect(screen.getByTestId('card-strength-recommended')).toHaveTextContent('추천') - expect(screen.getByTestId('card-strength-consider')).toHaveTextContent('검토') - expect(screen.getByTestId('card-strength-skip')).toHaveTextContent('보류') - fireEvent.click(screen.getByTestId('card-table-missing')) - fireEvent.click(screen.getByTestId('card-table')) - fireEvent.click(screen.getByTestId('card-toggle')) - fireEvent.click(screen.getByTestId('card-distinct-invalid')) - fireEvent.click(screen.getByTestId('card-distinct')) - fireEvent.click(screen.getByTestId('card-apply')) - fireEvent.click(screen.getByTestId('card-apply-duplicate')) - fireEvent.click(screen.getByTestId('card-apply-no-columns')) - fireEvent.click(screen.getByTestId('card-apply-empty')) - fireEvent.click(screen.getByTestId('card-apply-second')) - fireEvent.click(screen.getByTestId('card-close')) - - fireEvent.click(screen.getByRole('button', { name: 'DDL 내보내기' })) - for (const id of ['export-copy-ddl', 'export-svg', 'export-uml', 'export-mermaid', 'export-dbml', 'export-csv', 'export-md']) { - fireEvent.click(screen.getByTestId(id)) + vi.runOnlyPendingTimers(); + await Promise.resolve(); + }); + expect(screen.getByText("정렬 완료", { exact: false })).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "정렬 되돌리기" })); + expect( + screen.getByText("되돌렸습니다", { exact: false }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByTestId("flow-connect")); + fireEvent.click(screen.getByTestId("edge-label")); + fireEvent.click(screen.getByTestId("edge-submit")); + fireEvent.click(screen.getByTestId("flow-edge")); + fireEvent.click(screen.getByTestId("edge-cancel")); + fireEvent.click(screen.getByTestId("flow-edge-unlabeled")); + fireEvent.click(screen.getByTestId("edge-cancel")); + fireEvent.click(screen.getByTestId("flow-edge")); + vi.spyOn(window, "confirm") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + fireEvent.click(screen.getByTestId("edge-delete")); + fireEvent.click(screen.getByTestId("edge-delete")); + + fireEvent.doubleClick(screen.getByTestId("flow-node")); + fireEvent.submit(screen.getByTestId("table-empty-form")); + fireEvent.click(screen.getByTestId("table-cancel")); + fireEvent.doubleClick(screen.getByTestId("flow-node")); + fireEvent.submit(screen.getByTestId("table-form")); + fireEvent.doubleClick(screen.getByTestId("flow-node")); + fireEvent.click(screen.getByTestId("table-cancel")); + fireEvent.doubleClick(screen.getByTestId("flow-node")); + vi.spyOn(window, "confirm") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + fireEvent.click(screen.getByTestId("table-delete")); + fireEvent.click(screen.getByTestId("table-delete")); + }); + + it("adds nodes and exercises groups, cardinality, exports, inference, and clearing", async () => { + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!); + fireEvent.click(screen.getByTestId("add-name")); + fireEvent.click(screen.getByTestId("add-submit")); + expect(screen.getByTestId("node-count")).toHaveTextContent("1"); + + fireEvent.click(screen.getByRole("button", { name: "업무 그룹" })); + fireEvent.click(screen.getByTestId("group-create-guard")); + fireEvent.click(screen.getByTestId("group-name")); + fireEvent.click(screen.getByTestId("group-create")); + fireEvent.click(screen.getByTestId("group-assign-missing")); + fireEvent.click(screen.getByTestId("group-assign")); + vi.spyOn(window, "confirm") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + fireEvent.click(screen.getByTestId("group-delete")); + fireEvent.click(screen.getByTestId("group-delete")); + fireEvent.click(screen.getByTestId("group-close")); + + fireEvent.click( + screen.getByRole("button", { name: "인덱스 카디널리티 계산" }), + ); + expect(screen.getByTestId("card-format")).toHaveTextContent("50%"); + expect(screen.getByTestId("card-strength-recommended")).toHaveTextContent( + "추천", + ); + expect(screen.getByTestId("card-strength-consider")).toHaveTextContent( + "검토", + ); + expect(screen.getByTestId("card-strength-skip")).toHaveTextContent("보류"); + fireEvent.click(screen.getByTestId("card-table-missing")); + fireEvent.click(screen.getByTestId("card-table")); + fireEvent.click(screen.getByTestId("card-toggle")); + fireEvent.click(screen.getByTestId("card-distinct-invalid")); + fireEvent.click(screen.getByTestId("card-distinct")); + fireEvent.click(screen.getByTestId("card-apply")); + fireEvent.click(screen.getByTestId("card-apply-duplicate")); + fireEvent.click(screen.getByTestId("card-apply-no-columns")); + fireEvent.click(screen.getByTestId("card-apply-empty")); + fireEvent.click(screen.getByTestId("card-apply-second")); + fireEvent.click(screen.getByTestId("card-close")); + + fireEvent.click(screen.getByRole("button", { name: "DDL 내보내기" })); + for (const id of [ + "export-copy-ddl", + "export-svg", + "export-uml", + "export-mermaid", + "export-dbml", + "export-csv", + "export-md", + ]) { + fireEvent.click(screen.getByTestId(id)); } - fireEvent.click(screen.getByTestId('share-create')) - await waitFor(() => expect(screen.getByTestId('share-url')).toHaveTextContent('/api/share/one')) - fireEvent.click(screen.getByTestId('share-copy')) - fireEvent.click(screen.getByTestId('export-close')) - expect(exports.downloadText).toHaveBeenCalledTimes(6) - - fireEvent.click(screen.getByRole('button', { name: '관계 자동 추론' })) - expect(exports.inferRelationships).toHaveBeenCalled() - exports.inferRelationships.mockReturnValueOnce([]) - fireEvent.click(screen.getByRole('button', { name: '관계 자동 추론' })) - vi.spyOn(window, 'confirm').mockReturnValueOnce(false).mockReturnValueOnce(true) - fireEvent.click(screen.getByRole('button', { name: '모든 노드 지우기' })) - fireEvent.click(screen.getByRole('button', { name: '모든 노드 지우기' })) - expect(screen.getByText('ERD 캔버스가 비어 있습니다')).toBeInTheDocument() - }) - - it('covers guarded editor actions, navigation callbacks, and form selectors', async () => { - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - - vi.spyOn(window, 'confirm').mockReturnValue(true) + fireEvent.click(screen.getByTestId("share-create")); + await waitFor(() => + expect(screen.getByTestId("share-url")).toHaveTextContent( + "/api/share/one", + ), + ); + fireEvent.click(screen.getByTestId("share-copy")); + fireEvent.click(screen.getByTestId("export-close")); + expect(exports.downloadText).toHaveBeenCalledTimes(6); + + fireEvent.click(screen.getByRole("button", { name: "관계 자동 추론" })); + expect(exports.inferRelationships).toHaveBeenCalled(); + exports.inferRelationships.mockReturnValueOnce([]); + fireEvent.click(screen.getByRole("button", { name: "관계 자동 추론" })); + vi.spyOn(window, "confirm") + .mockReturnValueOnce(false) + .mockReturnValueOnce(true); + fireEvent.click(screen.getByRole("button", { name: "모든 노드 지우기" })); + fireEvent.click(screen.getByRole("button", { name: "모든 노드 지우기" })); + expect(screen.getByText("ERD 캔버스가 비어 있습니다")).toBeInTheDocument(); + }); + + it("covers guarded editor actions, navigation callbacks, and form selectors", async () => { + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + + vi.spyOn(window, "confirm").mockReturnValue(true); for (const id of [ - 'edge-guard-submit', - 'edge-guard-delete', - 'share-copy-guard', - 'table-delete-guard', - 'add-guard', - 'group-create-guard', - 'card-skip-guard', + "edge-guard-submit", + "edge-guard-delete", + "share-copy-guard", + "table-delete-guard", + "add-guard", + "group-create-guard", + "card-skip-guard", ]) { - fireEvent.click(screen.getByTestId(id)) + fireEvent.click(screen.getByTestId(id)); } - fireEvent.submit(screen.getByTestId('table-submit-guard')) + fireEvent.submit(screen.getByTestId("table-submit-guard")); for (const name of [ - 'ERD 자동 정렬', - '정렬 되돌리기', - '업무 그룹', - '인덱스 카디널리티 계산', + "ERD 자동 정렬", + "정렬 되돌리기", + "업무 그룹", + "인덱스 카디널리티 계산", ]) { - const button = screen.getByRole('button', { name }) as HTMLButtonElement - forceClick(button) + const button = screen.getByRole("button", { name }) as HTMLButtonElement; + forceClick(button); } - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - fireEvent.click(screen.getByTestId('export-close')) - - fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p2' } }) - fireEvent.change(screen.getByLabelText('Connection'), { target: { value: 'c1' } }) - fireEvent.change(screen.getByLabelText('New connection (DSN)'), { target: { value: 'Analytics' } }) - - fireEvent.change(screen.getByLabelText('New project'), { target: { value: ' ' } }) - forceClick(screen.getByRole('button', { name: 'Create' })) - fireEvent.change(screen.getByLabelText('New project'), { target: { value: 'demo' } }) - - fireEvent.change(screen.getByLabelText('Project'), { target: { value: '' } }) - forceClick(screen.getByRole('button', { name: 'Save connection' })) - fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p1' } }) - fireEvent.change(screen.getByLabelText('New connection (DSN)'), { target: { value: ' ' } }) - forceClick(screen.getByRole('button', { name: 'Save connection' })) - fireEvent.change(screen.getByLabelText('Connection'), { target: { value: '' } }) - forceClick(screen.getByRole('button', { name: 'Reverse engineer → snapshot' })) - fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!) - fireEvent.click(screen.getByTestId('add-cancel')) - - fireEvent.click(screen.getByRole('button', { name: '대시보드' })) - fireEvent.click(screen.getByRole('button', { name: /Billing.*다이어그램 보기/ })) - expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '대시보드' })) - await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) - expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '대시보드' })) - fireEvent.click(screen.getByRole('button', { name: '편집기로 이동' })) - expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '대시보드' })) - fireEvent.click(screen.getByRole('button', { name: '새 모델링' })) - expect(screen.getByRole('toolbar', { name: 'ERD 캔버스 도구' })).toBeInTheDocument() - - fireEvent.click(screen.getByRole('button', { name: '프로젝트' })) - fireEvent.change(screen.getByLabelText('새 프로젝트 이름'), { target: { value: 'Roadmap' } }) - fireEvent.click(screen.getByRole('button', { name: '새 프로젝트' })) - await waitFor(() => expect(api.createProject).toHaveBeenCalledWith('Roadmap')) - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) - expect(screen.getByRole('heading', { name: '다이어그램' })).toBeInTheDocument() - fireEvent.click(screen.getByRole('button', { name: '대시보드' })) - fireEvent.click(screen.getByRole('button', { name: '목록 보기' })) - }) - - it('clears replacement copy timers and pending timers during close and unmount', async () => { - vi.useFakeTimers() + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + fireEvent.click(screen.getByTestId("export-close")); + + fireEvent.change(screen.getByLabelText("Project"), { + target: { value: "p2" }, + }); + fireEvent.change(screen.getByLabelText("Connection"), { + target: { value: "c1" }, + }); + fireEvent.change(screen.getByLabelText("New connection (DSN)"), { + target: { value: "Analytics" }, + }); + + fireEvent.change(screen.getByLabelText("New project"), { + target: { value: " " }, + }); + forceClick(screen.getByRole("button", { name: "Create" })); + fireEvent.change(screen.getByLabelText("New project"), { + target: { value: "demo" }, + }); + + fireEvent.change(screen.getByLabelText("Project"), { + target: { value: "" }, + }); + forceClick(screen.getByRole("button", { name: "Save connection" })); + fireEvent.change(screen.getByLabelText("Project"), { + target: { value: "p1" }, + }); + fireEvent.change(screen.getByLabelText("New connection (DSN)"), { + target: { value: " " }, + }); + forceClick(screen.getByRole("button", { name: "Save connection" })); + fireEvent.change(screen.getByLabelText("Connection"), { + target: { value: "" }, + }); + forceClick( + screen.getByRole("button", { name: "Reverse engineer → snapshot" }), + ); + fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!); + fireEvent.click(screen.getByTestId("add-cancel")); + + fireEvent.click(screen.getByRole("button", { name: "대시보드" })); + fireEvent.click( + screen.getByRole("button", { name: /Billing.*다이어그램 보기/ }), + ); + expect( + screen.getByRole("heading", { name: "다이어그램" }), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "대시보드" })); + await waitFor(() => + expect( + screen.getAllByRole("button", { name: "열기" }).length, + ).toBeGreaterThan(0), + ); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); + expect( + screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "대시보드" })); + fireEvent.click(screen.getByRole("button", { name: "편집기로 이동" })); + expect( + screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "대시보드" })); + fireEvent.click(screen.getByRole("button", { name: "새 모델링" })); + expect( + screen.getByRole("toolbar", { name: "ERD 캔버스 도구" }), + ).toBeInTheDocument(); + + fireEvent.click(screen.getByRole("button", { name: "프로젝트" })); + fireEvent.change(screen.getByLabelText("새 프로젝트 이름"), { + target: { value: "Roadmap" }, + }); + fireEvent.click(screen.getByRole("button", { name: "새 프로젝트" })); + await waitFor(() => + expect(api.createProject).toHaveBeenCalledWith("Roadmap"), + ); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); + expect( + screen.getByRole("heading", { name: "다이어그램" }), + ).toBeInTheDocument(); + fireEvent.click(screen.getByRole("button", { name: "대시보드" })); + fireEvent.click(screen.getByRole("button", { name: "목록 보기" })); + }); + + it("clears replacement copy timers and pending timers during close and unmount", async () => { + vi.useFakeTimers(); + await act(async () => { + render(); + await Promise.resolve(); + await Promise.resolve(); + }); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!); + fireEvent.click(screen.getByTestId("add-name")); + fireEvent.click(screen.getByTestId("add-submit")); + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + fireEvent.click(screen.getByTestId("export-copy-ddl")); + fireEvent.click(screen.getByTestId("export-copy-ddl")); + fireEvent.click(screen.getByTestId("share-create")); + await act(async () => { + await Promise.resolve(); + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("share-copy")); await act(async () => { - render() - await Promise.resolve() - await Promise.resolve() - }) - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!) - fireEvent.click(screen.getByTestId('add-name')) - fireEvent.click(screen.getByTestId('add-submit')) - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - fireEvent.click(screen.getByTestId('export-copy-ddl')) - fireEvent.click(screen.getByTestId('export-copy-ddl')) - fireEvent.click(screen.getByTestId('share-create')) + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("share-copy")); await act(async () => { - await Promise.resolve() - await Promise.resolve() - }) - fireEvent.click(screen.getByTestId('share-copy')) - await act(async () => { await Promise.resolve() }) - fireEvent.click(screen.getByTestId('share-copy')) - await act(async () => { await Promise.resolve() }) + await Promise.resolve(); + }); await act(async () => { - vi.advanceTimersByTime(2000) - await Promise.resolve() - }) - fireEvent.click(screen.getByTestId('export-close')) - - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - fireEvent.click(screen.getByTestId('export-copy-ddl')) - fireEvent.click(screen.getByTestId('share-create')) - await act(async () => { await Promise.resolve() }) - fireEvent.click(screen.getByTestId('share-copy')) - await act(async () => { await Promise.resolve() }) - fireEvent.click(screen.getByTestId('export-close')) - - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - fireEvent.click(screen.getByTestId('export-copy-ddl')) - fireEvent.click(screen.getByTestId('share-create')) - await act(async () => { await Promise.resolve() }) - fireEvent.click(screen.getByTestId('share-copy')) - await act(async () => { await Promise.resolve() }) - cleanup() - }) - - it('ignores authentication completions after unmount', async () => { - let resolveMe!: (value: any) => void - api.getMe.mockReturnValueOnce(new Promise((resolve) => { resolveMe = resolve })) - render() - cleanup() - await act(async () => resolveMe({ subject: 'late', display_name: 'Late' })) - - let rejectMe!: (reason: unknown) => void - api.getMe.mockReturnValueOnce(new Promise((_resolve, reject) => { rejectMe = reject })) - render() - cleanup() - await act(async () => rejectMe(new Error('late failure'))) - }) - - it('logs auto-layout failures and preserves nodes added after the undo snapshot', async () => { - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - vi.useFakeTimers() - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) + vi.advanceTimersByTime(2000); + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("export-close")); + + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + fireEvent.click(screen.getByTestId("export-copy-ddl")); + fireEvent.click(screen.getByTestId("share-create")); + await act(async () => { + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("share-copy")); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - vi.useRealTimers() - - const consoleError = vi.spyOn(console, 'error').mockImplementation(() => {}) - vi.stubGlobal('requestAnimationFrame', () => { throw new Error('frame unavailable') }) - fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' })) - await waitFor(() => expect(screen.getByText('정렬에 실패했습니다. 다시 시도해 주세요.', { exact: false })).toBeInTheDocument()) - expect(consoleError).toHaveBeenCalled() - - vi.stubGlobal('requestAnimationFrame', (callback: FrameRequestCallback) => { callback(0); return 1 }) - fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' })) - await screen.findByText('정렬 완료', { exact: false }) - fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!) - fireEvent.click(screen.getByTestId('add-name')) - fireEvent.click(screen.getByTestId('add-submit')) - fireEvent.click(screen.getByRole('button', { name: '정렬 되돌리기' })) - expect(screen.getByTestId('node-count')).toHaveTextContent('3') - }) - - it('shows terminal refresh failures from the polling loop', async () => { + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("export-close")); + + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + fireEvent.click(screen.getByTestId("export-copy-ddl")); + fireEvent.click(screen.getByTestId("share-create")); + await act(async () => { + await Promise.resolve(); + }); + fireEvent.click(screen.getByTestId("share-copy")); + await act(async () => { + await Promise.resolve(); + }); + cleanup(); + }); + + it("ignores authentication completions after unmount", async () => { + let resolveMe!: (value: any) => void; + api.getMe.mockReturnValueOnce( + new Promise((resolve) => { + resolveMe = resolve; + }), + ); + render(); + cleanup(); + await act(async () => resolveMe({ subject: "late", display_name: "Late" })); + + let rejectMe!: (reason: unknown) => void; + api.getMe.mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectMe = reject; + }), + ); + render(); + cleanup(); + await act(async () => rejectMe(new Error("late failure"))); + }); + + it("logs auto-layout failures and preserves nodes added after the undo snapshot", async () => { + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + vi.useFakeTimers(); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); + await act(async () => { + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + vi.useRealTimers(); + + const consoleError = vi + .spyOn(console, "error") + .mockImplementation(() => {}); + vi.stubGlobal("requestAnimationFrame", () => { + throw new Error("frame unavailable"); + }); + fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" })); + await waitFor(() => + expect( + screen.getByText("정렬에 실패했습니다. 다시 시도해 주세요.", { + exact: false, + }), + ).toBeInTheDocument(), + ); + expect(consoleError).toHaveBeenCalled(); + + vi.stubGlobal("requestAnimationFrame", (callback: FrameRequestCallback) => { + callback(0); + return 1; + }); + fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" })); + await screen.findByText("정렬 완료", { exact: false }); + fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!); + fireEvent.click(screen.getByTestId("add-name")); + fireEvent.click(screen.getByTestId("add-submit")); + fireEvent.click(screen.getByRole("button", { name: "정렬 되돌리기" })); + expect(screen.getByTestId("node-count")).toHaveTextContent("3"); + }); + + it("shows terminal refresh failures from the polling loop", async () => { api.listSnapshots .mockResolvedValueOnce(snapshots) - .mockRejectedValueOnce(new Error('terminal refresh down')) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - vi.useFakeTimers() - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) + .mockRejectedValueOnce(new Error("terminal refresh down")); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + vi.useFakeTimers(); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - expect(screen.getByRole('alert')).toHaveTextContent('terminal refresh down') - }) - - it('ignores stale project metadata failures after changing projects', async () => { - let rejectConnections!: (reason: unknown) => void - let rejectSnapshots!: (reason: unknown) => void + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole("alert")).toHaveTextContent( + "terminal refresh down", + ); + }); + + it("ignores stale project metadata failures after changing projects", async () => { + let rejectConnections!: (reason: unknown) => void; + let rejectSnapshots!: (reason: unknown) => void; api.listConnections - .mockReturnValueOnce(new Promise((_resolve, reject) => { rejectConnections = reject })) - .mockResolvedValueOnce(connections) + .mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectConnections = reject; + }), + ) + .mockResolvedValueOnce(connections); api.listSnapshots - .mockReturnValueOnce(new Promise((_resolve, reject) => { rejectSnapshots = reject })) - .mockResolvedValueOnce(snapshots) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - fireEvent.change(screen.getByLabelText('Project'), { target: { value: 'p2' } }) + .mockReturnValueOnce( + new Promise((_resolve, reject) => { + rejectSnapshots = reject; + }), + ) + .mockResolvedValueOnce(snapshots); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + fireEvent.change(screen.getByLabelText("Project"), { + target: { value: "p2" }, + }); await act(async () => { - rejectConnections(new Error('stale connections')) - rejectSnapshots(new Error('stale snapshots')) - await Promise.resolve() - }) - expect(screen.queryByText(/stale (connections|snapshots)/)).not.toBeInTheDocument() - }) - - it('renders snapshot failures and polls without a selected project', async () => { + rejectConnections(new Error("stale connections")); + rejectSnapshots(new Error("stale snapshots")); + await Promise.resolve(); + }); + expect( + screen.queryByText(/stale (connections|snapshots)/), + ).not.toBeInTheDocument(); + }); + + it("renders snapshot failures and polls without a selected project", async () => { api.getSnapshot.mockResolvedValue({ - schema_snapshot_uuid: 's3', - status: 'failed', + schema_snapshot_uuid: "s3", + status: "failed", schema_filter: null, - error_message: 'database rejected snapshot', + error_message: "database rejected snapshot", snapshot_json: null, - }) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - fireEvent.change(screen.getByLabelText('Connection DSN'), { target: { value: 'postgresql://db.example/test' } }) - fireEvent.click(screen.getByRole('button', { name: 'Save connection' })) - await waitFor(() => expect(api.createConnection).toHaveBeenCalled()) - fireEvent.click(screen.getByRole('button', { name: 'Reverse engineer → snapshot' })) - await waitFor(() => expect(api.createSnapshot).toHaveBeenCalledWith('p1', 'c2', undefined)) - - vi.useFakeTimers() - fireEvent.change(screen.getByLabelText('Project'), { target: { value: '' } }) + }); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + fireEvent.change(screen.getByLabelText("Connection DSN"), { + target: { value: "postgresql://db.example/test" }, + }); + fireEvent.click(screen.getByRole("button", { name: "Save connection" })); + await waitFor(() => expect(api.createConnection).toHaveBeenCalled()); + fireEvent.click( + screen.getByRole("button", { name: "Reverse engineer → snapshot" }), + ); + await waitFor(() => + expect(api.createSnapshot).toHaveBeenCalledWith("p1", "c2", undefined), + ); + + vi.useFakeTimers(); + fireEvent.change(screen.getByLabelText("Project"), { + target: { value: "" }, + }); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - expect(screen.getByRole('alert')).toHaveTextContent('database rejected snapshot') - }) - - it('renders user identity fallbacks and a diagram list without a project label', async () => { - api.getMe.mockResolvedValueOnce({ subject: 'subject-only', display_name: null }) - await renderReadyApp() - expect(screen.getByText('subject-only')).toBeInTheDocument() - - cleanup() - api.getMe.mockResolvedValueOnce({ subject: '', display_name: null }) - await renderReadyApp() - expect(screen.getByText('인증 필요')).toBeInTheDocument() - - cleanup() - const onOpenEditor = vi.fn() + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByRole("alert")).toHaveTextContent( + "database rejected snapshot", + ); + }); + + it("renders user identity fallbacks and a diagram list without a project label", async () => { + api.getMe.mockResolvedValueOnce({ + subject: "subject-only", + display_name: null, + }); + await renderReadyApp(); + expect(screen.getByText("subject-only")).toBeInTheDocument(); + + cleanup(); + api.getMe.mockResolvedValueOnce({ subject: "", display_name: null }); + await renderReadyApp(); + expect(screen.getByText("인증 필요")).toBeInTheDocument(); + + cleanup(); + const onOpenEditor = vi.fn(); render( , - ) - expect(screen.getAllByText('현재 프로젝트')).toHaveLength(2) - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) - expect(onOpenEditor).toHaveBeenCalledWith('s1') - }) - - it('ignores duplicate share creation while a request is pending', async () => { - let resolveShare!: (value: { url: string }) => void - api.createShareLink.mockReturnValueOnce(new Promise((resolve) => { resolveShare = resolve })) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - fireEvent.click(screen.getByTestId('share-create')) - fireEvent.click(screen.getByTestId('share-create')) - expect(api.createShareLink).toHaveBeenCalledTimes(1) - await act(async () => resolveShare({ url: 'http://localhost/api/share/done' })) - }) - - it('preserves positions across graph refresh and applies recommendations with sibling nodes', async () => { - let pollCount = 0 + ); + expect(screen.getAllByText("현재 프로젝트")).toHaveLength(2); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); + expect(onOpenEditor).toHaveBeenCalledWith("s1"); + }); + + it("ignores duplicate share creation while a request is pending", async () => { + let resolveShare!: (value: { url: string }) => void; + api.createShareLink.mockReturnValueOnce( + new Promise((resolve) => { + resolveShare = resolve; + }), + ); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + fireEvent.click(screen.getByTestId("share-create")); + fireEvent.click(screen.getByTestId("share-create")); + expect(api.createShareLink).toHaveBeenCalledTimes(1); + await act(async () => + resolveShare({ url: "http://localhost/api/share/done" }), + ); + }); + + it("preserves positions across graph refresh and applies recommendations with sibling nodes", async () => { + let pollCount = 0; api.getSnapshot.mockImplementation(async () => ({ - schema_snapshot_uuid: 's3', - status: pollCount++ === 0 ? 'running' : 'succeeded', - schema_filter: 'public', + schema_snapshot_uuid: "s3", + status: pollCount++ === 0 ? "running" : "succeeded", + schema_filter: "public", error_message: null, - snapshot_json: { relations: [], columns: [], pk_columns: [], fk_edges: [] }, - })) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - vi.useFakeTimers() - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) + snapshot_json: { + relations: [], + columns: [], + pk_columns: [], + fk_edges: [], + }, + })); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + vi.useFakeTimers(); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - expect(screen.getByTestId('node-count')).toHaveTextContent('2') + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + expect(screen.getByTestId("node-count")).toHaveTextContent("2"); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - vi.useRealTimers() - expect(screen.getByTestId('node-count')).toHaveTextContent('2') - - fireEvent.click(screen.getByRole('button', { name: '업무 그룹' })) - fireEvent.click(screen.getByTestId('group-name')) - fireEvent.click(screen.getByTestId('group-create')) - fireEvent.click(screen.getByTestId('group-assign')) - vi.spyOn(window, 'confirm').mockReturnValue(true) - fireEvent.click(screen.getByTestId('group-delete')) - fireEvent.click(screen.getByTestId('group-close')) - - fireEvent.click(screen.getByRole('button', { name: '인덱스 카디널리티 계산' })) - fireEvent.click(screen.getByTestId('card-table')) - fireEvent.click(screen.getByTestId('card-apply')) - fireEvent.click(screen.getByTestId('card-clear-apply')) - }) - - it('falls back to node ids when auto-layout receives legacy nodes without titles', async () => { + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + vi.useRealTimers(); + expect(screen.getByTestId("node-count")).toHaveTextContent("2"); + + fireEvent.click(screen.getByRole("button", { name: "업무 그룹" })); + fireEvent.click(screen.getByTestId("group-name")); + fireEvent.click(screen.getByTestId("group-create")); + fireEvent.click(screen.getByTestId("group-assign")); + vi.spyOn(window, "confirm").mockReturnValue(true); + fireEvent.click(screen.getByTestId("group-delete")); + fireEvent.click(screen.getByTestId("group-close")); + + fireEvent.click( + screen.getByRole("button", { name: "인덱스 카디널리티 계산" }), + ); + fireEvent.click(screen.getByTestId("card-table")); + fireEvent.click(screen.getByTestId("card-apply")); + fireEvent.click(screen.getByTestId("card-clear-apply")); + }); + + it("falls back to node ids when auto-layout receives legacy nodes without titles", async () => { vi.mocked(snapshotToGraph).mockReturnValueOnce({ nodes: [ - { id: 'z-node', type: 'tableNode', position: { x: 0, y: 0 }, data: { columns: [], badges: { pk: false, fk: false } } }, - { id: 'a-node', type: 'tableNode', position: { x: 1, y: 1 }, data: { columns: [], badges: { pk: false, fk: false } } }, + { + id: "z-node", + type: "tableNode", + position: { x: 0, y: 0 }, + data: { columns: [], badges: { pk: false, fk: false } }, + }, + { + id: "a-node", + type: "tableNode", + position: { x: 1, y: 1 }, + data: { columns: [], badges: { pk: false, fk: false } }, + }, ] as any, edges: [], - }) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - vi.useFakeTimers() - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) + }); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + vi.useFakeTimers(); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - await Promise.resolve() - }) - vi.useRealTimers() - fireEvent.click(screen.getByRole('button', { name: 'ERD 자동 정렬' })) - await screen.findByText('정렬 완료', { exact: false }) - }) - - it('reports API effect failures, snapshot polling failures, share failures, and clipboard failures', async () => { - api.listConnections.mockRejectedValueOnce(new Error('connections down')) - api.listSnapshots.mockRejectedValueOnce(new Error('snapshots down')) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '편집기' })) - expect(await screen.findByRole('alert')).toHaveTextContent(/down/) - fireEvent.click(screen.getAllByRole('button', { name: '테이블 추가' })[0]!) - fireEvent.click(screen.getByTestId('add-name')) - fireEvent.click(screen.getByTestId('add-submit')) - fireEvent.click(screen.getByRole('button', { name: '공유 및 내보내기' })) - api.createShareLink.mockRejectedValueOnce(new Error('share down')) - fireEvent.click(screen.getByTestId('share-create')) - await waitFor(() => expect(screen.getByTestId('share-error')).toHaveTextContent('share down')) - - api.createShareLink.mockResolvedValueOnce({ url: 'http://localhost/api/share/fail-copy' }) - fireEvent.click(screen.getByTestId('share-create')) - await waitFor(() => expect(screen.getByTestId('share-url')).toHaveTextContent('fail-copy')) - vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce(new Error('copy down')) - fireEvent.click(screen.getByTestId('share-copy')) - await waitFor(() => expect(screen.getByTestId('share-error')).toHaveTextContent('복사에 실패')) - - cleanup() - vi.useRealTimers() - api.listConnections.mockResolvedValue(connections) - api.listSnapshots.mockResolvedValue(snapshots) - await renderReadyApp() - fireEvent.click(screen.getByRole('button', { name: '다이어그램' })) - await waitFor(() => expect(screen.getAllByRole('button', { name: '열기' }).length).toBeGreaterThan(0)) - vi.useFakeTimers() - api.getSnapshot.mockRejectedValueOnce(new Error('poll down')) - fireEvent.click(screen.getAllByRole('button', { name: '열기' })[0]!) + vi.advanceTimersByTime(1000); + await Promise.resolve(); + await Promise.resolve(); + }); + vi.useRealTimers(); + fireEvent.click(screen.getByRole("button", { name: "ERD 자동 정렬" })); + await screen.findByText("정렬 완료", { exact: false }); + }); + + it("reports API effect failures, snapshot polling failures, share failures, and clipboard failures", async () => { + api.listConnections.mockRejectedValueOnce(new Error("connections down")); + api.listSnapshots.mockRejectedValueOnce(new Error("snapshots down")); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "편집기" })); + expect(await screen.findByRole("alert")).toHaveTextContent(/down/); + fireEvent.click(screen.getAllByRole("button", { name: "테이블 추가" })[0]!); + fireEvent.click(screen.getByTestId("add-name")); + fireEvent.click(screen.getByTestId("add-submit")); + fireEvent.click(screen.getByRole("button", { name: "공유 및 내보내기" })); + api.createShareLink.mockRejectedValueOnce(new Error("share down")); + fireEvent.click(screen.getByTestId("share-create")); + await waitFor(() => + expect(screen.getByTestId("share-error")).toHaveTextContent("share down"), + ); + + api.createShareLink.mockResolvedValueOnce({ + url: "http://localhost/api/share/fail-copy", + }); + fireEvent.click(screen.getByTestId("share-create")); + await waitFor(() => + expect(screen.getByTestId("share-url")).toHaveTextContent("fail-copy"), + ); + vi.mocked(navigator.clipboard.writeText).mockRejectedValueOnce( + new Error("copy down"), + ); + fireEvent.click(screen.getByTestId("share-copy")); + await waitFor(() => + expect(screen.getByTestId("share-error")).toHaveTextContent( + "복사에 실패", + ), + ); + + cleanup(); + vi.useRealTimers(); + api.listConnections.mockResolvedValue(connections); + api.listSnapshots.mockResolvedValue(snapshots); + await renderReadyApp(); + fireEvent.click(screen.getByRole("button", { name: "다이어그램" })); + await waitFor(() => + expect( + screen.getAllByRole("button", { name: "열기" }).length, + ).toBeGreaterThan(0), + ); + vi.useFakeTimers(); + api.getSnapshot.mockRejectedValueOnce(new Error("poll down")); + fireEvent.click(screen.getAllByRole("button", { name: "열기" })[0]!); await act(async () => { - vi.advanceTimersByTime(1000) - await Promise.resolve() - }) - expect(screen.getByRole('alert')).toHaveTextContent('poll down') - }) -}) + vi.advanceTimersByTime(1000); + await Promise.resolve(); + }); + expect(screen.getByRole("alert")).toHaveTextContent("poll down"); + }); +}); diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 0fa62ede3..278282dcd 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -72,7 +72,16 @@ const TERMINAL_SNAPSHOT_STATUSES = new Set([ "not_found", ]); -const SUPPORTED_DSN_PROTOCOLS = new Set(["postgres:", "postgresql:", "snowflake:"]); +const SUPPORTED_DSN_PROTOCOLS = new Set([ + "postgres:", + "postgresql:", + "snowflake:", +]); + +function sanitizeCssClass(str: unknown): string { + if (typeof str !== "string") return ""; + return str.replace(/[^a-zA-Z0-9-_]/g, ""); +} function sanitizeHtml(str: string | null | undefined): string { if (!str) return ""; @@ -168,7 +177,9 @@ export default function App() { const [shareLinkError, setShareLinkError] = useState(null); const [editingEdge, setEditingEdge] = useState(null); - const [editingNode, setEditingNode] = useState | null>(null); + const [editingNode, setEditingNode] = useState | null>( + null, + ); const [isEditTableModalOpen, setIsEditTableModalOpen] = useState(false); const [isAddTableModalOpen, setIsAddTableModalOpen] = useState(false); const [newTableName, setNewTableName] = useState(""); @@ -313,7 +324,11 @@ export default function App() { getSnapshot(snapshotId) .then((s) => { setSnapshot(s); - if (s.status === "succeeded" || s.status === "failed" || s.status === "not_found") { + if ( + s.status === "succeeded" || + s.status === "failed" || + s.status === "not_found" + ) { clearInterval(timer); if (selectedProjectId) { listSnapshots(selectedProjectId) @@ -367,9 +382,7 @@ export default function App() { // ⚡ Bolt: Removed nodesById Map creation inside useMemo which iterates over all nodes and allocates memory. // Using nodes.find() for single lookups is O(N) but avoids Map construction overhead, providing ~10x speedup and reducing GC pressure. const cardinalityNode = useMemo(() => { - return ( - nodes.find((n) => n.id === cardinalityTableId) ?? nodes[0] ?? null - ); + return nodes.find((n) => n.id === cardinalityTableId) ?? nodes[0] ?? null; }, [cardinalityTableId, nodes]); const cardinalityColumns = useMemo(() => { if (!cardinalityNode) return []; @@ -380,11 +393,7 @@ export default function App() { cardinalityDistinctCounts[column.column_name] ?? "", ), })); - }, [ - cardinalityColumnSelections, - cardinalityDistinctCounts, - cardinalityNode, - ]); + }, [cardinalityColumnSelections, cardinalityDistinctCounts, cardinalityNode]); const cardinalityRecommendations = useMemo( () => buildIndexRecommendations({ @@ -392,7 +401,11 @@ export default function App() { rowCount: cardinalityRowCountNumber, columns: cardinalityColumns, }), - [cardinalityColumns, cardinalityNode?.data.title, cardinalityRowCountNumber], + [ + cardinalityColumns, + cardinalityNode?.data.title, + cardinalityRowCountNumber, + ], ); const appliedCardinalityIndexes = useMemo( () => cardinalityNode?.data.indexes ?? [], @@ -404,7 +417,8 @@ export default function App() { const columns = new Set(); for (const index of appliedCardinalityIndexes) { if (index.index_name) names.add(index.index_name); - if (index.columns && index.columns.length > 0) columns.add(index.columns.join(",")); + if (index.columns && index.columns.length > 0) + columns.add(index.columns.join(",")); } return { names, columns }; }, [appliedCardinalityIndexes]); @@ -503,11 +517,14 @@ export default function App() { } } - const onNodeDoubleClick = useCallback((event: React.MouseEvent, node: Node) => { - event.preventDefault(); - setEditingNode(node as Node); - setIsEditTableModalOpen(true); - }, []); + const onNodeDoubleClick = useCallback( + (event: React.MouseEvent, node: Node) => { + event.preventDefault(); + setEditingNode(node as Node); + setIsEditTableModalOpen(true); + }, + [], + ); const onEdgeClick = useCallback((event: React.MouseEvent, edge: Edge) => { event.preventDefault(); @@ -622,7 +639,11 @@ export default function App() { } function onDownloadMermaid() { - downloadText("pg-erd-diagram.mermaid", exportMermaid(nodes, edges), "text/plain"); + downloadText( + "pg-erd-diagram.mermaid", + exportMermaid(nodes, edges), + "text/plain", + ); } function onDownloadDbml() { @@ -630,7 +651,11 @@ export default function App() { } function onDownloadPrisma() { - downloadText("pg-erd-diagram.prisma", exportPrisma(nodes, edges), "text/plain"); + downloadText( + "pg-erd-diagram.prisma", + exportPrisma(nodes, edges), + "text/plain", + ); } function onExportDictionaryCsv() { @@ -696,10 +721,7 @@ export default function App() { })); } - function onCardinalityDistinctCountChange( - columnName: string, - value: string, - ) { + function onCardinalityDistinctCountChange(columnName: string, value: string) { if (!/^\d*$/.test(value)) return; setCardinalityDistinctCounts((prev) => ({ ...prev, @@ -729,7 +751,8 @@ export default function App() { const recColumns = recommendation.columns?.join(",") ?? ""; if ( - (recommendation.index_name && appliedIndexNames.has(recommendation.index_name)) || + (recommendation.index_name && + appliedIndexNames.has(recommendation.index_name)) || (recColumns && appliedColumns.has(recColumns)) ) { return currentNodes; @@ -806,7 +829,12 @@ export default function App() { } function onDeleteBusinessGroup(groupId: string) { - if (!window.confirm("이 그룹을 삭제하면 포함된 모든 테이블에서 그룹 지정이 해제됩니다. 정말로 삭제하시겠습니까?")) return; + if ( + !window.confirm( + "이 그룹을 삭제하면 포함된 모든 테이블에서 그룹 지정이 해제됩니다. 정말로 삭제하시겠습니까?", + ) + ) + return; setBusinessGroups((groups) => groups.filter((group) => group.id !== groupId), ); @@ -825,7 +853,6 @@ export default function App() { ); } - function onDeleteTable() { if (!editingNode) return; if (!window.confirm("정말로 이 테이블을 삭제하시겠습니까?")) return; @@ -834,7 +861,11 @@ export default function App() { setNodes((nds) => nds.filter((n) => n.id !== editingNode.id)); // Remove connected edges - setEdges((eds) => eds.filter((e) => e.source !== editingNode.id && e.target !== editingNode.id)); + setEdges((eds) => + eds.filter( + (e) => e.source !== editingNode.id && e.target !== editingNode.id, + ), + ); setIsEditTableModalOpen(false); setEditingNode(null); @@ -851,7 +882,9 @@ export default function App() { if (!title.trim()) return; // Parse columns from formData - const updatedColumns: Array["data"]["columns"][number]> = []; + const updatedColumns: Array< + Node["data"]["columns"][number] + > = []; for (let i = 0; i < editingNode.data.columns.length; i++) { const colName = formData.get(`col_name_${i}`) as string; if (colName === null) continue; // Deleted column @@ -881,13 +914,13 @@ export default function App() { columns: updatedColumns, badges: { ...n.data.badges, - pk: updatedColumns.some(c => c.is_pk) - } - } + pk: updatedColumns.some((c) => c.is_pk), + }, + }, }; } return n; - }) + }), ); setIsEditTableModalOpen(false); @@ -899,7 +932,6 @@ export default function App() { setEditingNode(null); } - function onAddTableSubmit() { if (!newTableName.trim()) return; const newId = `new_table_${Date.now()}`; @@ -964,7 +996,9 @@ export default function App() { /* v8 ignore next -- the save control is disabled until both fields are present */ if (!nextConnectionName || !connectionDsn) return; if (!isSupportedConnectionDsn(connectionDsn)) { - setError("Connection DSN must use postgresql://, postgres://, or snowflake:// with a host."); + setError( + "Connection DSN must use postgresql://, postgres://, or snowflake:// with a host.", + ); dsnInput.value = ""; setIsDsnPresent(false); return; @@ -1006,12 +1040,7 @@ export default function App() { if (isAuthLoading) { return ( -
+

pg-erd-cloud

Authenticating…

@@ -1020,7 +1049,8 @@ export default function App() { if (!me) { /* v8 ignore next -- the loaded unauthenticated state always records its rejection */ - const authGateMessage = authError ?? "Sign in before managing database metadata."; + const authGateMessage = + authError ?? "Sign in before managing database metadata."; return (

Authentication required

@@ -1045,7 +1075,11 @@ export default function App() { + + {createProjectHint ? ( + + {createProjectHint} + + ) : null} + + +
+ +
+ + -
- + {connections.map((c) => ( + + ))} + + + +
{ + e.preventDefault(); + onCreateConnection(); + }} + > + + setConnName(e.target.value)} + placeholder="name" + /> + + setIsDsnPresent(Boolean(e.currentTarget.value.trim())) + } + placeholder="postgresql://... or snowflake://..." + aria-label="Connection DSN" + /> + + {createConnectionHint ? ( + + {createConnectionHint} + + ) : null} +
+ +
+ + setSchemaFilter(e.target.value)} + placeholder="public" + /> +
-
- -
- setProjectName(e.target.value)} - /> -
- {createProjectHint ? ( - - {createProjectHint} - - ) : null} -
- -
- -
- - -
- -
- - setConnName(e.target.value)} - placeholder="name" - /> - - setIsDsnPresent(Boolean(e.currentTarget.value.trim())) - } - placeholder="postgresql://... or snowflake://..." - aria-label="Connection DSN" - /> - - {createConnectionHint ? ( - - {createConnectionHint} - - ) : null} -
- -
- - setSchemaFilter(e.target.value)} - placeholder="public" - /> -
- - - {createSnapshotHint ? ( - - {createSnapshotHint} - - ) : null} - -
- Snapshot: {snapshot?.status || "—"} - {snapshot?.error_message ? ( -
- {String(snapshot.error_message)} + {createSnapshotHint ? ( + + {createSnapshotHint} + + ) : null} + +
+ Snapshot: {snapshot?.status || "—"} + {snapshot?.error_message ? ( +
+ {String(snapshot.error_message)} +
+ ) : null}
- ) : null} -
- {error ? ( -
- {error} -
- ) : null} + {error ? ( +
+ {error} +
+ ) : null} ) : (
@@ -1228,7 +1279,10 @@ export default function App() {
{activeView === "dashboard" ? ( -
+

대시보드

@@ -1254,7 +1308,10 @@ export default function App() {
-
+

최근 프로젝트

) : ( -
아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요.
+
+ 아직 프로젝트가 없습니다. 편집기에서 프로젝트를 생성하세요. +
)}
-
+

최근 다이어그램

{ setSnapshotId(id); setSnapshot(null); @@ -1307,33 +1371,55 @@ export default function App() {

프로젝트

-

프로젝트를 선택하면 해당 다이어그램 목록을 볼 수 있습니다.

+

+ 프로젝트를 선택하면 해당 다이어그램 목록을 볼 수 있습니다. +

-
+
{ + e.preventDefault(); + onCreateProject(); + }} + > setProjectName(event.currentTarget.value)} + onChange={(event) => + setProjectName(event.currentTarget.value) + } /> -
+
-
+
이름 연결 동작
{projects.map((project) => ( -
- {sanitizeHtml(project.project_name)} - {project.project_space_uuid === selectedProjectId ? connections.length : "선택 후 표시"} +
+ + {sanitizeHtml(project.project_name)} + + + {project.project_space_uuid === selectedProjectId + ? connections.length + : "선택 후 표시"} +
@@ -1357,22 +1445,30 @@ export default function App() {

다이어그램

-

{selectedProject ? `${selectedProject.project_name} 프로젝트의 스냅샷` : "프로젝트를 선택하세요."}

+

+ {selectedProject + ? `${selectedProject.project_name} 프로젝트의 스냅샷` + : "프로젝트를 선택하세요."} +

- +
e.preventDefault()}> + +
) : (
-
- - - - - - - - - - - + + + + + + + + + + + +
+ {[layoutMessage, nodeSearchStatus].filter(Boolean).join(" ")} +
+
+ + { + reactFlowRef.current = instance; + }} > - IMG - - + + )} +
+ )} + + 0} + hasDictionaryExport={nodes.length > 0} + hasDiagramExport={nodes.length > 0} + shareLinkUrl={shareLinkUrl} + isCreatingShareLink={isCreatingShareLink} + isShareLinkCopied={isShareLinkCopied} + shareLinkError={shareLinkError} + canCreateShareLink={Boolean(selectedProjectId)} + onCloseExport={onCloseExport} + onCopyExportDdl={onCopyExportDdl} + onDownloadSvg={onDownloadSvg} + onDownloadUml={onDownloadUml} + onDownloadMermaid={onDownloadMermaid} + onExportDictionaryCsv={onExportDictionaryCsv} + onExportDictionaryMarkdown={onExportDictionaryMarkdown} + onDownloadDbml={onDownloadDbml} + onDownloadPrisma={onDownloadPrisma} + onCreateShareLink={onCreateShareLink} + onCopyShareLink={onCopyShareLink} + /> + + + + + + - UML - - -
- {[layoutMessage, nodeSearchStatus].filter(Boolean).join(" ")} -
-
+ parsePositiveInteger={parsePositiveInteger} + calculateCardinalityRatio={calculateCardinalityRatio} + formatPercent={formatPercent} + strengthLabel={strengthLabel} + /> - { - reactFlowRef.current = instance; - }} - > - - - - - - {nodes.length === 0 && ( -
- {isSnapshotPending ? ( - <> - + + + +
)}
@@ -1734,11 +1850,17 @@ export function DiagramTable({ 동작 {rows.map(({ item, name }) => ( -
+
{name} {selectedProjectName || "현재 프로젝트"} - + {sanitizeHtml(item.status)}