From eddd92c39292fa4fa2e33a94310f7bb2658c36bb Mon Sep 17 00:00:00 2001 From: Donghao Ren Date: Fri, 29 Aug 2025 11:08:24 -0700 Subject: [PATCH 1/2] test: add tests for wasm packages --- .github/workflows/ci.yml | 3 + .prettierignore | 3 +- package-lock.json | 1026 ++++++++++++++++- package.json | 1 + packages/density-clustering/package.json | 9 +- .../test/clustering.test.ts | 214 ++++ packages/density-clustering/vitest.config.ts | 11 + packages/umap-wasm/index.d.ts | 2 +- packages/umap-wasm/index.js | 2 +- packages/umap-wasm/package.json | 8 +- packages/umap-wasm/test/umap.test.ts | 234 ++++ packages/umap-wasm/vitest.config.ts | 11 + scripts/test.sh | 13 + 13 files changed, 1522 insertions(+), 15 deletions(-) create mode 100644 packages/density-clustering/test/clustering.test.ts create mode 100644 packages/density-clustering/vitest.config.ts create mode 100644 packages/umap-wasm/test/umap.test.ts create mode 100644 packages/umap-wasm/vitest.config.ts create mode 100755 scripts/test.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b67dad1c..96908b75 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -61,6 +61,9 @@ jobs: - name: Build JavaScript and Python packages run: ./scripts/build.sh + - name: Run tests + run: ./scripts/test.sh + - name: Upload node package artifact uses: actions/upload-artifact@v4 with: diff --git a/.prettierignore b/.prettierignore index a5dafaf3..5149ebef 100644 --- a/.prettierignore +++ b/.prettierignore @@ -12,6 +12,5 @@ packages/docs/demo-data/ packages/umap-wasm/runtime.js packages/umap-wasm/notebooks/data/ packages/umap-wasm/third_party/ -packages/component/src/lib/density_clustering/wasm/ -packages/density-clustering/comparisons/supercluster-result.json +packages/density-clustering/density_clustering_wasm/pkg/ packages/backend/embedding_atlas/static/ \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index e5e990ad..edbecc26 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2747,6 +2747,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/chai": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.2.tgz", + "integrity": "sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*" + } + }, "node_modules/@types/command-line-args": { "version": "5.2.3", "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", @@ -2804,6 +2814,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -3009,6 +3026,131 @@ "@uwdata/mosaic-sql": "^0.18.0" } }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/mocker/node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/@volar/language-core": { "version": "2.4.22", "resolved": "https://registry.npmjs.org/@volar/language-core/-/language-core-2.4.22.tgz", @@ -3549,6 +3691,16 @@ "node": ">=8" } }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/async": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", @@ -3556,6 +3708,29 @@ "dev": true, "license": "MIT" }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/axios": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.11.0.tgz", + "integrity": "sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, "node_modules/axobject-query": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", @@ -3573,6 +3748,117 @@ "dev": true, "license": "MIT" }, + "node_modules/binary-install": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/binary-install/-/binary-install-1.1.2.tgz", + "integrity": "sha512-ZS2cqFHPZOy4wLxvzqfQvDjCOifn+7uCPqNmYRIBM/03+yllON+4fNnsD0VJdW0p97y+E+dTRNPStWNqMBq+9g==", + "deprecated": "Package no longer supported. Contact Support at https://www.npmjs.com/support for more info.", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^0.26.1", + "rimraf": "^3.0.2", + "tar": "^6.1.11" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/binary-install/node_modules/axios": { + "version": "0.26.1", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.26.1.tgz", + "integrity": "sha512-fPwcX4EvnSHuInCMItEhAGnaSEXRBjtzh9fOtsE6E1G6p7vl7edEeZe11QHf18+6+9gR5PbKV/sGKNaD8YaMeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.14.8" + } + }, + "node_modules/binary-install/node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/binary-install/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/binary-install/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/binary-install/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/binary-install/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/binary-install/node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/binary-install/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, "node_modules/binary-search-bounds": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/binary-search-bounds/-/binary-search-bounds-2.0.5.tgz", @@ -3620,6 +3906,32 @@ "node": ">=8" } }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/ccount": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", @@ -3631,6 +3943,23 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -3684,6 +4013,16 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/check-error": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", + "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, "node_modules/chokidar": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", @@ -3763,6 +4102,21 @@ "simple-swizzle": "^0.2.2" } }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/comma-separated-tokens": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", @@ -3845,6 +4199,13 @@ "dev": true, "license": "MIT" }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, "node_modules/confbox": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", @@ -4315,6 +4676,16 @@ } } }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -4380,6 +4751,18 @@ "robust-predicates": "^3.0.2" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -4438,6 +4821,23 @@ "resolved": "packages/docs", "link": true }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", @@ -4515,6 +4915,46 @@ "node": ">= 0.4" } }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/es6-error": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/es6-error/-/es6-error-4.1.1.tgz", @@ -4601,6 +5041,16 @@ "dev": true, "license": "MIT" }, + "node_modules/expect-type": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.2.tgz", + "integrity": "sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/exsolve": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.0.7.tgz", @@ -4786,6 +5236,27 @@ "tabbable": "^6.2.0" } }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, "node_modules/foreground-child": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", @@ -4802,6 +5273,25 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.4.tgz", + "integrity": "sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/fs-extra": { "version": "11.3.1", "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.1.tgz", @@ -4817,6 +5307,46 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs-minipass/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true, + "license": "ISC" + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -4842,6 +5372,49 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/gh-pages": { "version": "6.3.0", "resolved": "https://registry.npmjs.org/gh-pages/-/gh-pages-6.3.0.tgz", @@ -5026,17 +5599,50 @@ "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "license": "MIT", "engines": { - "node": ">=8" + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/has-property-descriptors": { + "node_modules/has-tostringtag": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { - "es-define-property": "^1.0.0" + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -5163,6 +5769,25 @@ "node": ">=8" } }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, "node_modules/internmap": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", @@ -5704,6 +6329,13 @@ "loose-envify": "cli.js" } }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, "node_modules/lru-cache": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz", @@ -5772,6 +6404,18 @@ "node": ">=10" } }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", @@ -5912,6 +6556,33 @@ "node": ">=8.6" } }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, "node_modules/mini-svg-data-uri": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", @@ -6093,6 +6764,16 @@ "node": ">= 0.4" } }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, "node_modules/oniguruma-to-es": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-3.1.1.tgz", @@ -6241,6 +6922,16 @@ "node": ">=8" } }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/path-key": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", @@ -6290,6 +6981,16 @@ "dev": true, "license": "MIT" }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, "node_modules/perfect-debounce": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", @@ -6490,6 +7191,15 @@ "node": ">=12.0.0" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/publint": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.12.tgz", @@ -6717,6 +7427,69 @@ "dev": true, "license": "MIT" }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, "node_modules/roarr": { "version": "2.15.4", "resolved": "https://registry.npmjs.org/roarr/-/roarr-2.15.4.tgz", @@ -7004,6 +7777,13 @@ "@types/hast": "^3.0.4" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, "node_modules/signal-exit": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", @@ -7103,6 +7883,20 @@ "dev": true, "license": "BSD-3-Clause" }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", + "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", + "dev": true, + "license": "MIT" + }, "node_modules/streamlit-component-lib": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/streamlit-component-lib/-/streamlit-component-lib-2.0.0.tgz", @@ -7487,6 +8281,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strip-literal": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.0.0.tgz", + "integrity": "sha512-TcccoMhJOM3OebGhSBEmp3UZ2SfDMZUEBdRA/9ynfLi8yYajyWX3JiXArcJt4Umh4vISpspkQIY8ZZoCqjbviA==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/strip-literal/node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-outer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", @@ -7705,6 +8519,13 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, "node_modules/tinyexec": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.1.tgz", @@ -7757,6 +8578,36 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.3.tgz", + "integrity": "sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -8160,6 +9011,29 @@ } } }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/vite-plugin-dts": { "version": "4.5.4", "resolved": "https://registry.npmjs.org/vite-plugin-dts/-/vite-plugin-dts-4.5.4.tgz", @@ -8791,6 +9665,99 @@ } } }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/vitest/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/vitest/node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, "node_modules/vscode-uri": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/vscode-uri/-/vscode-uri-3.1.0.tgz", @@ -8820,6 +9787,20 @@ } } }, + "node_modules/wasm-pack": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/wasm-pack/-/wasm-pack-0.13.1.tgz", + "integrity": "sha512-P9exD4YkjpDbw68xUhF3MDm/CC/3eTmmthyG5bHJ56kalxOTewOunxTke4SyF8MTXV6jUtNjXggPgrGmMtczGg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT OR Apache-2.0", + "dependencies": { + "binary-install": "^1.0.1" + }, + "bin": { + "wasm-pack": "run.js" + } + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -8842,6 +9823,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wordwrapjs": { "version": "5.1.0", "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.0.tgz", @@ -8939,6 +9937,13 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, "node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", @@ -9012,7 +10017,11 @@ }, "packages/density-clustering": { "name": "@embedding-atlas/density-clustering", - "version": "0.0.0" + "version": "0.0.0", + "devDependencies": { + "vitest": "^3.2.4", + "wasm-pack": "^0.13.1" + } }, "packages/docs": { "devDependencies": { @@ -9094,7 +10103,10 @@ }, "packages/umap-wasm": { "name": "@embedding-atlas/umap-wasm", - "version": "0.0.0" + "version": "0.0.0", + "devDependencies": { + "vitest": "^3.2.4" + } }, "packages/viewer": { "name": "@embedding-atlas/viewer", diff --git a/package.json b/package.json index a3b1d2fc..d0166610 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ ], "scripts": { "build": "./scripts/build.sh", + "test": "./scripts/test.sh", "check-format": "prettier -c ." }, "devDependencies": { diff --git a/packages/density-clustering/package.json b/packages/density-clustering/package.json index fe1db411..282088d6 100644 --- a/packages/density-clustering/package.json +++ b/packages/density-clustering/package.json @@ -7,11 +7,16 @@ "module": "density_clustering_wasm/js/index.js", "type": "module", "scripts": { - "build": "npx -y wasm-pack build --release --target web density_clustering_wasm && rm density_clustering_wasm/pkg/.gitignore density_clustering_wasm/pkg/package.json" + "build": "wasm-pack build --release --target web density_clustering_wasm && rm density_clustering_wasm/pkg/.gitignore density_clustering_wasm/pkg/package.json", + "test": "vitest run" }, "files": [ "density_clustering_wasm/js/index.js", "density_clustering_wasm/pkg/density_clustering_wasm.js", "density_clustering_wasm/pkg/density_clustering_wasm_bg.wasm" - ] + ], + "devDependencies": { + "vitest": "^3.2.4", + "wasm-pack": "^0.13.1" + } } diff --git a/packages/density-clustering/test/clustering.test.ts b/packages/density-clustering/test/clustering.test.ts new file mode 100644 index 00000000..b6868c6a --- /dev/null +++ b/packages/density-clustering/test/clustering.test.ts @@ -0,0 +1,214 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import { findClusters, type FindClustersOptions } from "../density_clustering_wasm/js/index.js"; +import * as cluster from "../density_clustering_wasm/pkg/density_clustering_wasm.js"; + +describe("Density Clustering WebAssembly Module", () => { + let simpleDensityMap: Float32Array; + let width = 10; + let height = 10; + + beforeAll(async () => { + const isNode = typeof process !== "undefined" && !!process.versions?.node; + if (isNode) { + // In node we manually initialize the WASM module. + // Once bundled this should be handled properly. + let binary = require("fs").readFileSync( + new URL("../density_clustering_wasm/pkg/density_clustering_wasm_bg.wasm", import.meta.url), + ); + await cluster.default(binary); + } + + // Create a simple density map with two distinct clusters + simpleDensityMap = new Float32Array(width * height); + + // Fill with background density + simpleDensityMap.fill(0.1); + + // Create first cluster (top-left) + for (let y = 1; y < 4; y++) { + for (let x = 1; x < 4; x++) { + simpleDensityMap[y * width + x] = 1.0; + } + } + // Peak in first cluster + simpleDensityMap[2 * width + 2] = 2.0; + + // Create second cluster (bottom-right) + for (let y = 6; y < 9; y++) { + for (let x = 6; x < 9; x++) { + simpleDensityMap[y * width + x] = 0.8; + } + } + // Peak in second cluster + simpleDensityMap[7 * width + 7] = 1.5; + }); + + describe("findClusters", () => { + test("should find clusters with default options", async () => { + let clusters = await findClusters(simpleDensityMap, width, height); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + expect(clusters.length).toBeGreaterThan(0); + }); + + test("should return clusters with correct structure", async () => { + let clusters = await findClusters(simpleDensityMap, width, height); + + expect(clusters.length).toBeGreaterThan(0); + + let cluster = clusters[0]; + expect(cluster).toBeDefined(); + expect(typeof cluster.identifier).toBe("number"); + expect(typeof cluster.sumDensity).toBe("number"); + expect(typeof cluster.meanX).toBe("number"); + expect(typeof cluster.meanY).toBe("number"); + expect(typeof cluster.maxDensity).toBe("number"); + expect(Array.isArray(cluster.maxDensityLocation)).toBe(true); + expect(cluster.maxDensityLocation.length).toBe(2); + expect(typeof cluster.pixelCount).toBe("number"); + + if (cluster.boundary) { + expect(Array.isArray(cluster.boundary)).toBe(true); + expect( + cluster.boundary.every( + (polygon) => Array.isArray(polygon) && polygon.every((point) => Array.isArray(point) && point.length === 2), + ), + ).toBe(true); + } + + if (cluster.boundaryRectApproximation) { + expect(Array.isArray(cluster.boundaryRectApproximation)).toBe(true); + expect(cluster.boundaryRectApproximation.every((rect) => Array.isArray(rect) && rect.length === 4)).toBe(true); + } + }); + + test("should handle custom unionThreshold option", async () => { + let options: Partial = { + unionThreshold: 5, + }; + + let clusters = await findClusters(simpleDensityMap, width, height, options); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + + test("should handle different unionThreshold values", async () => { + let lowThreshold = await findClusters(simpleDensityMap, width, height, { unionThreshold: 1 }); + let highThreshold = await findClusters(simpleDensityMap, width, height, { unionThreshold: 20 }); + + expect(lowThreshold).toBeDefined(); + expect(highThreshold).toBeDefined(); + + // Different thresholds may produce different numbers of clusters + expect(lowThreshold.length).toBeGreaterThanOrEqual(0); + expect(highThreshold.length).toBeGreaterThanOrEqual(0); + }); + + test("should handle empty density map", async () => { + let emptyMap = new Float32Array(width * height).fill(0); + let clusters = await findClusters(emptyMap, width, height); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + + test("should handle uniform density map", async () => { + let uniformMap = new Float32Array(width * height).fill(0.5); + let clusters = await findClusters(uniformMap, width, height); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + + test("should handle single pixel density map", async () => { + let singlePixelMap = new Float32Array(1).fill(1.0); + let clusters = await findClusters(singlePixelMap, 1, 1); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + + test("should validate cluster properties", async () => { + let clusters = await findClusters(simpleDensityMap, width, height); + + for (let cluster of clusters) { + expect(cluster.sumDensity).toBeGreaterThan(0); + expect(cluster.maxDensity).toBeGreaterThan(0); + expect(cluster.pixelCount).toBeGreaterThan(0); + expect(cluster.meanX).toBeGreaterThanOrEqual(0); + expect(cluster.meanX).toBeLessThan(width); + expect(cluster.meanY).toBeGreaterThanOrEqual(0); + expect(cluster.meanY).toBeLessThan(height); + expect(cluster.maxDensityLocation[0]).toBeGreaterThanOrEqual(0); + expect(cluster.maxDensityLocation[0]).toBeLessThan(width); + expect(cluster.maxDensityLocation[1]).toBeGreaterThanOrEqual(0); + expect(cluster.maxDensityLocation[1]).toBeLessThan(height); + expect(cluster.maxDensity).toBeLessThanOrEqual(cluster.sumDensity); + } + }); + + test("should handle rectangular density maps", async () => { + let rectWidth = 15; + let rectHeight = 5; + let rectMap = new Float32Array(rectWidth * rectHeight); + + // Create a simple cluster in the middle + for (let y = 1; y < 4; y++) { + for (let x = 6; x < 10; x++) { + rectMap[y * rectWidth + x] = 1.0; + } + } + + let clusters = await findClusters(rectMap, rectWidth, rectHeight); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + + test("should be deterministic with same input", async () => { + let clusters1 = await findClusters(simpleDensityMap, width, height); + let clusters2 = await findClusters(simpleDensityMap, width, height); + + expect(clusters1.length).toBe(clusters2.length); + + // Sort clusters by identifier for comparison + clusters1.sort((a, b) => a.identifier - b.identifier); + clusters2.sort((a, b) => a.identifier - b.identifier); + + for (let i = 0; i < clusters1.length; i++) { + expect(clusters1[i].identifier).toBe(clusters2[i].identifier); + expect(clusters1[i].sumDensity).toBeCloseTo(clusters2[i].sumDensity, 5); + expect(clusters1[i].meanX).toBeCloseTo(clusters2[i].meanX, 5); + expect(clusters1[i].meanY).toBeCloseTo(clusters2[i].meanY, 5); + expect(clusters1[i].maxDensity).toBeCloseTo(clusters2[i].maxDensity, 5); + expect(clusters1[i].pixelCount).toBe(clusters2[i].pixelCount); + } + }); + + test("should handle complex density patterns", async () => { + let complexWidth = 20; + let complexHeight = 20; + let complexMap = new Float32Array(complexWidth * complexHeight); + + // Create a more complex pattern with multiple clusters + for (let y = 0; y < complexHeight; y++) { + for (let x = 0; x < complexWidth; x++) { + let distance1 = Math.sqrt((x - 5) ** 2 + (y - 5) ** 2); + let distance2 = Math.sqrt((x - 15) ** 2 + (y - 15) ** 2); + let distance3 = Math.sqrt((x - 5) ** 2 + (y - 15) ** 2); + + let density = Math.max(Math.exp(-distance1 / 3), Math.exp(-distance2 / 4), Math.exp(-distance3 / 2)) * 0.1; + + complexMap[y * complexWidth + x] = density; + } + } + + let clusters = await findClusters(complexMap, complexWidth, complexHeight); + + expect(clusters).toBeDefined(); + expect(Array.isArray(clusters)).toBe(true); + }); + }); +}); diff --git a/packages/density-clustering/vitest.config.ts b/packages/density-clustering/vitest.config.ts new file mode 100644 index 00000000..b9c33154 --- /dev/null +++ b/packages/density-clustering/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + testTimeout: 1000, + include: ["test/**/*.test.{js,ts}"], + exclude: ["node_modules/**/*"], + }, +}); diff --git a/packages/umap-wasm/index.d.ts b/packages/umap-wasm/index.d.ts index e690a3d9..79149f0a 100644 --- a/packages/umap-wasm/index.d.ts +++ b/packages/umap-wasm/index.d.ts @@ -82,7 +82,7 @@ export interface KNNOptions { export interface KNNQueryResult { indices: Int32Array; - distnaces: Float32Array; + distances: Float32Array; } export interface KNN { diff --git a/packages/umap-wasm/index.js b/packages/umap-wasm/index.js index 0d9c5103..2886de93 100644 --- a/packages/umap-wasm/index.js +++ b/packages/umap-wasm/index.js @@ -159,7 +159,7 @@ export async function createKNN(count, input_dim, data, options) { queryByVector(data, k) { let [ptr_i, ptr_d] = get_buffers(k); ctx.f32_array(ptr_q, input_dim).set(data); - let rk = ctx.knn_context_query_by_index(ptr_knn, ptr_q, k, ptr_i, ptr_d); + let rk = ctx.knn_context_query_by_vector(ptr_knn, ptr_q, k, ptr_i, ptr_d); return { indices: ctx.i32_array(ptr_i, rk).slice(), distances: ctx.f32_array(ptr_d, rk).slice(), diff --git a/packages/umap-wasm/package.json b/packages/umap-wasm/package.json index 4dfc8e4d..4becedf6 100644 --- a/packages/umap-wasm/package.json +++ b/packages/umap-wasm/package.json @@ -7,10 +7,14 @@ "module": "index.js", "type": "module", "scripts": { - "build": "make" + "build": "make", + "test": "vitest run" }, "files": [ "index.js", "runtime.js" - ] + ], + "devDependencies": { + "vitest": "^3.2.4" + } } diff --git a/packages/umap-wasm/test/umap.test.ts b/packages/umap-wasm/test/umap.test.ts new file mode 100644 index 00000000..8cdefeaa --- /dev/null +++ b/packages/umap-wasm/test/umap.test.ts @@ -0,0 +1,234 @@ +import { beforeAll, describe, expect, test } from "vitest"; +import { createKNN, createUMAP, type UMAPOptions } from "../index.js"; + +describe("UMAP WebAssembly Module", () => { + let testData: Float32Array; + let count = 100; + let inputDim = 4; + let outputDim = 2; + + beforeAll(() => { + // Generate test data: simple 4D points + testData = new Float32Array(count * inputDim); + for (let i = 0; i < count; i++) { + for (let j = 0; j < inputDim; j++) { + testData[i * inputDim + j] = Math.random() * 10; + } + } + }); + + describe("createUMAP", () => { + test("should create UMAP instance with default options", async () => { + let umap = await createUMAP(count, inputDim, outputDim, testData); + + expect(umap).toBeDefined(); + expect(umap.inputDim).toBe(inputDim); + expect(umap.outputDim).toBe(outputDim); + expect(umap.epoch).toBe(0); + + umap.destroy(); + }); + + test("should create UMAP instance with custom options", async () => { + let options: UMAPOptions = { + metric: "cosine", + nNeighbors: 10, + minDist: 0.5, + spread: 2.0, + nEpochs: 100, + seed: 42, + }; + + let umap = await createUMAP(count, inputDim, outputDim, testData, options); + expect(umap).toBeDefined(); + umap.destroy(); + }); + + test("should return valid embedding array", async () => { + let umap = await createUMAP(count, inputDim, outputDim, testData); + + let embedding = umap.embedding; + expect(embedding).toBeInstanceOf(Float32Array); + expect(embedding.length).toBe(count * outputDim); + umap.destroy(); + }); + + test("should run UMAP algorithm", async () => { + let umap = await createUMAP(count, inputDim, outputDim, testData, { nEpochs: 10 }); + + let initialEpoch = umap.epoch; + umap.run(5); + + expect(umap.epoch).toBeGreaterThan(initialEpoch); + expect(umap.epoch).toBeLessThanOrEqual(5); + + umap.destroy(); + }); + + test("should run to completion when no epoch limit specified", async () => { + let umap = await createUMAP(count, inputDim, outputDim, testData, { nEpochs: 5 }); + + umap.run(); + + expect(umap.epoch).toBe(5); + + umap.destroy(); + }); + + test("should handle different metrics", async () => { + let euclideanUmap = await createUMAP(count, inputDim, outputDim, testData, { metric: "euclidean" }); + let cosineUmap = await createUMAP(count, inputDim, outputDim, testData, { metric: "cosine" }); + + expect(euclideanUmap).toBeDefined(); + expect(cosineUmap).toBeDefined(); + + euclideanUmap.destroy(); + cosineUmap.destroy(); + }); + + test("should handle different KNN methods", async () => { + let hnswUmap = await createUMAP(count, inputDim, outputDim, testData, { knnMethod: "hnsw" }); + let vptreeUmap = await createUMAP(count, inputDim, outputDim, testData, { knnMethod: "vptree" }); + + expect(hnswUmap).toBeDefined(); + expect(vptreeUmap).toBeDefined(); + + hnswUmap.destroy(); + vptreeUmap.destroy(); + }); + + test("should handle different initialization methods", async () => { + let spectralUmap = await createUMAP(count, inputDim, outputDim, testData, { initializeMethod: "spectral" }); + let randomUmap = await createUMAP(count, inputDim, outputDim, testData, { initializeMethod: "random" }); + + expect(spectralUmap).toBeDefined(); + expect(randomUmap).toBeDefined(); + + spectralUmap.destroy(); + randomUmap.destroy(); + }); + + test("should produce consistent results with same seed", async () => { + let options: UMAPOptions = { seed: 42, nEpochs: 10 }; + + let umap1 = await createUMAP(count, inputDim, outputDim, testData, options); + umap1.run(); + let embedding1 = new Float32Array(umap1.embedding); + umap1.destroy(); + + let umap2 = await createUMAP(count, inputDim, outputDim, testData, options); + umap2.run(); + let embedding2 = new Float32Array(umap2.embedding); + umap2.destroy(); + + // Results should be identical with same seed + for (let i = 0; i < embedding1.length; i++) { + expect(embedding1[i]).toBeCloseTo(embedding2[i], 5); + } + }); + + test("should reduce dimensionality correctly", async () => { + let higherDimInput = 10; + let lowerDimOutput = 3; + let testCount = 50; + + let higherDimData = new Float32Array(testCount * higherDimInput); + for (let i = 0; i < testCount * higherDimInput; i++) { + higherDimData[i] = Math.random() * 10; + } + + let umap = await createUMAP(testCount, higherDimInput, lowerDimOutput, higherDimData); + umap.run(); + + let embedding = umap.embedding; + expect(embedding.length).toBe(testCount * lowerDimOutput); + + // Verify all values are finite + for (let i = 0; i < embedding.length; i++) { + expect(Number.isFinite(embedding[i])).toBe(true); + } + + umap.destroy(); + }); + + test("should handle small datasets", async () => { + let smallCount = 10; + let smallData = new Float32Array(smallCount * inputDim); + for (let i = 0; i < smallCount * inputDim; i++) { + smallData[i] = Math.random() * 10; + } + + let umap = await createUMAP(smallCount, inputDim, outputDim, smallData); + umap.run(); + + let embedding = umap.embedding; + expect(embedding.length).toBe(smallCount * outputDim); + + umap.destroy(); + }); + }); + + describe("createKNN", () => { + test("should create KNN instance", async () => { + let knn = await createKNN(count, inputDim, testData, { metric: "euclidean" }); + + expect(knn).toBeDefined(); + expect(typeof knn.queryByIndex).toBe("function"); + expect(typeof knn.queryByVector).toBe("function"); + + knn.destroy(); + }); + + test("should query by index", async () => { + let knn = await createKNN(count, inputDim, testData, { metric: "euclidean" }); + + let result = knn.queryByIndex(0, 5); + + expect(result).toBeDefined(); + expect(result.indices).toBeInstanceOf(Int32Array); + expect(result.distances).toBeInstanceOf(Float32Array); + expect(result.indices.length).toBeLessThanOrEqual(5); + expect(result.distances.length).toBeLessThanOrEqual(5); + expect(result.indices.length).toBe(result.distances.length); + + knn.destroy(); + }); + + test("should query by vector", async () => { + let knn = await createKNN(count, inputDim, testData, { metric: "euclidean" }); + + let queryVector = new Float32Array([1.0, 2.0, 3.0, 4.0]); + let result = knn.queryByVector(queryVector, 3); + + expect(result).toBeDefined(); + expect(result.indices).toBeInstanceOf(Int32Array); + expect(result.distances).toBeInstanceOf(Float32Array); + expect(result.indices.length).toBeLessThanOrEqual(3); + expect(result.distances.length).toBeLessThanOrEqual(3); + + knn.destroy(); + }); + + test("should handle different KNN methods", async () => { + let hnswKnn = await createKNN(count, inputDim, testData, { method: "hnsw" }); + let vptreeKnn = await createKNN(count, inputDim, testData, { method: "vptree" }); + + expect(hnswKnn).toBeDefined(); + expect(vptreeKnn).toBeDefined(); + + hnswKnn.destroy(); + vptreeKnn.destroy(); + }); + + test("should handle different metrics", async () => { + let euclideanKnn = await createKNN(count, inputDim, testData, { metric: "euclidean" }); + let cosineKnn = await createKNN(count, inputDim, testData, { metric: "cosine" }); + + expect(euclideanKnn).toBeDefined(); + expect(cosineKnn).toBeDefined(); + + euclideanKnn.destroy(); + cosineKnn.destroy(); + }); + }); +}); diff --git a/packages/umap-wasm/vitest.config.ts b/packages/umap-wasm/vitest.config.ts new file mode 100644 index 00000000..b9c33154 --- /dev/null +++ b/packages/umap-wasm/vitest.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + globals: true, + testTimeout: 1000, + include: ["test/**/*.test.{js,ts}"], + exclude: ["node_modules/**/*"], + }, +}); diff --git a/scripts/test.sh b/scripts/test.sh new file mode 100755 index 00000000..a89e419e --- /dev/null +++ b/scripts/test.sh @@ -0,0 +1,13 @@ +#!/bin/bash + +# Build WASM modules + +set -euxo pipefail + +pushd packages/density-clustering +npm run test +popd + +pushd packages/umap-wasm +npm run test +popd \ No newline at end of file From ae48dc6a0169eb5ed738ea0797df0cca36dde169 Mon Sep 17 00:00:00 2001 From: Donghao Ren Date: Fri, 29 Aug 2025 15:30:03 -0700 Subject: [PATCH 2/2] Update test.sh --- scripts/test.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/test.sh b/scripts/test.sh index a89e419e..e0bd699c 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -1,6 +1,6 @@ #!/bin/bash -# Build WASM modules +# Run tests set -euxo pipefail @@ -10,4 +10,4 @@ popd pushd packages/umap-wasm npm run test -popd \ No newline at end of file +popd