From 2debd34022eef0bbd179d2b42013040ceef3384d Mon Sep 17 00:00:00 2001 From: Usman Imamu Date: Wed, 2 Sep 2026 20:33:51 +0100 Subject: [PATCH] =?UTF-8?q?Revert=20"fix:=20repair=20broken=20merges,=20de?= =?UTF-8?q?ad=20SSE=20reconnect=20loop,=20and=20make=20suite=20gr=E2=80=A6?= =?UTF-8?q?"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 00e8f202359d52c7c59d3ff9e02375dce423dfe6. --- eslint.config.mjs | 10 - package-lock.json | 1778 +++++++++++++++++++++++ package.json | 2 + src/features/outages/components/Dry.tsx | 219 +++ src/lib/outages.ts | 2 +- src/lib/session-sse.ts | 28 +- tests/auth-flow.test.tsx | 12 +- tests/contract.test.ts | 13 +- tests/payment-detail-drawer.test.tsx | 10 +- tests/payments-view.test.tsx | 6 +- tests/session-sse.test.ts | 117 +- tests/settings-theme.test.tsx | 2 +- tests/settings-wallet.test.tsx | 33 +- tests/useOutagesTableState.test.ts | 12 +- vitest.setup.ts | 20 - 15 files changed, 2099 insertions(+), 165 deletions(-) create mode 100644 src/features/outages/components/Dry.tsx diff --git a/eslint.config.mjs b/eslint.config.mjs index 586cd29..40d7bd1 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -14,16 +14,6 @@ const eslintConfig = defineConfig([ "next-env.d.ts", ]), { - // Typed linting rules only work where type information exists. Scope them - // to app sources (covered by tsconfig.json) so config files like this one - // don't crash the lint run. - files: ["src/**/*.{ts,tsx}", "tests/**/*.{ts,tsx}"], - languageOptions: { - parserOptions: { - projectService: true, - tsconfigRootDir: import.meta.dirname, - }, - }, rules: { "@typescript-eslint/no-explicit-any": "error", "@typescript-eslint/no-unsafe-assignment": "warn", diff --git a/package-lock.json b/package-lock.json index ac64ca6..43cb1e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,9 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "driver.js": "^1.8.0", + "install": "^0.13.0", "next": "16.1.4", + "npm": "^11.17.0", "react": "^19.2.7", "react-dom": "19.2.7", "tailwind-merge": "^3.4.0" @@ -576,6 +578,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.2.3", @@ -586,6 +589,7 @@ "version": "1.11.3", "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -2651,6 +2655,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "@emnapi/wasi-threads": "1.2.1", @@ -2662,6 +2667,7 @@ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "tslib": "^2.4.0" @@ -7502,6 +7508,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/install": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/install/-/install-0.13.0.tgz", + "integrity": "sha512-zDml/jzr2PKU9I8J/xyZBQn8rPCAY//UOYNmR01XwNwyfhEWObo2SWfSl1+0tm1u6PhxLwDnfsT/6jB7OUxqFA==", + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, "node_modules/internal-slot": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", @@ -8911,6 +8926,1769 @@ "node": ">=18" } }, + "node_modules/npm": { + "version": "11.17.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-11.17.0.tgz", + "integrity": "sha512-PurxiZexEHDTE4SSaLI3ZrnbAGiZfeyUcQcxcP5D+hfytNAze/D1IzDuInTn9XVLIbAQUnQuSPXJx02LHjLvQw==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/metavuln-calculator", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which" + ], + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^9.8.0", + "@npmcli/config": "^10.11.0", + "@npmcli/fs": "^5.0.0", + "@npmcli/map-workspaces": "^5.0.3", + "@npmcli/metavuln-calculator": "^9.0.3", + "@npmcli/package-json": "^7.0.5", + "@npmcli/promise-spawn": "^9.0.1", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.4", + "@sigstore/tuf": "^4.0.2", + "abbrev": "^4.0.0", + "archy": "~1.0.0", + "cacache": "^20.0.4", + "chalk": "^5.6.2", + "ci-info": "^4.4.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^13.0.6", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^9.0.3", + "ini": "^6.0.0", + "init-package-json": "^8.2.5", + "is-cidr": "^6.0.4", + "json-parse-even-better-errors": "^5.0.0", + "libnpmaccess": "^10.0.3", + "libnpmdiff": "^8.1.10", + "libnpmexec": "^10.3.0", + "libnpmfund": "^7.0.24", + "libnpmorg": "^8.0.1", + "libnpmpack": "^9.1.10", + "libnpmpublish": "^11.2.0", + "libnpmsearch": "^9.0.1", + "libnpmteam": "^8.0.2", + "libnpmversion": "^8.0.4", + "make-fetch-happen": "^15.0.6", + "minimatch": "^10.2.5", + "minipass": "^7.1.3", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^12.4.0", + "nopt": "^9.0.0", + "npm-audit-report": "^7.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.2", + "npm-pick-manifest": "^11.0.3", + "npm-profile": "^12.0.1", + "npm-registry-fetch": "^19.1.1", + "npm-user-validate": "^4.0.0", + "p-map": "^7.0.4", + "pacote": "^21.5.1", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.1.0", + "qrcode-terminal": "^0.12.0", + "read": "^5.0.1", + "semver": "^7.8.4", + "spdx-expression-parse": "^4.0.0", + "ssri": "^13.0.1", + "supports-color": "^10.2.2", + "tar": "^7.5.16", + "text-table": "~0.2.0", + "tiny-relative-date": "^2.0.2", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^7.0.2", + "which": "^6.0.1" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@gar/promise-retry": { + "version": "1.0.3", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "4.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^11.2.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "9.8.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^5.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/metavuln-calculator": "^9.0.2", + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/query": "^5.0.0", + "@npmcli/redact": "^4.0.0", + "@npmcli/run-script": "^10.0.0", + "bin-links": "^6.0.0", + "cacache": "^20.0.1", + "common-ancestor-path": "^2.0.0", + "hosted-git-info": "^9.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^11.2.1", + "minimatch": "^10.0.3", + "nopt": "^9.0.0", + "npm-install-checks": "^8.0.0", + "npm-package-arg": "^13.0.0", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "pacote": "^21.0.2", + "parse-conflict-json": "^5.0.1", + "proc-log": "^6.0.0", + "proggy": "^4.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "semver": "^7.3.7", + "ssri": "^13.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^4.0.0" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "10.11.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "ini": "^6.0.0", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "ini": "^6.0.0", + "lru-cache": "^11.2.1", + "npm-pick-manifest": "^11.0.1", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^5.0.0", + "npm-normalize-package-bin": "^5.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "5.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "glob": "^13.0.0", + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "9.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^20.0.0", + "json-parse-even-better-errors": "^5.0.0", + "pacote": "^21.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "7.0.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "glob": "^13.0.0", + "hosted-git-info": "^9.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.5.3", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "10.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^5.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "node-gyp": "^12.1.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/bundle": { + "version": "4.0.0", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/core": { + "version": "3.2.1", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.5.1", + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/sign": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@gar/promise-retry": "^1.0.2", + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.0", + "@sigstore/protobuf-specs": "^0.5.0", + "make-fetch-happen": "^15.0.4", + "proc-log": "^6.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "4.0.2", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.5.0", + "tuf-js": "^4.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@sigstore/verify": { + "version": "3.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/@tufjs/models": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^10.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.1.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/npm/node_modules/bin-links": { + "version": "6.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "proc-log": "^6.0.0", + "read-cmd-shim": "^6.0.0", + "write-file-atomic": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "5.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "20.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^5.0.0", + "fs-minipass": "^3.0.0", + "glob": "^13.0.0", + "lru-cache": "^11.1.0", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.6.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "3.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.4.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "5.0.5", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "2.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "8.0.4", + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.3", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "13.0.6", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "9.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.7.2", + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "8.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^10.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "8.2.5", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "npm-package-arg": "^13.0.0", + "promzard": "^3.0.1", + "read": "^5.0.1", + "semver": "^7.7.2", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "10.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "6.0.4", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^5.0.4" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "4.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=20" + } + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "5.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "10.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "8.1.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.8.0", + "@npmcli/installed-package-contents": "^4.0.0", + "binary-extensions": "^3.0.0", + "diff": "^8.0.2", + "minimatch": "^10.0.3", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "tar": "^7.5.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "10.3.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/arborist": "^9.8.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2", + "proc-log": "^6.0.0", + "read": "^5.0.1", + "semver": "^7.3.7", + "signal-exit": "^4.1.0", + "walk-up-path": "^4.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "7.0.24", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.8.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "8.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "9.1.10", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^9.8.0", + "@npmcli/run-script": "^10.0.0", + "npm-package-arg": "^13.0.0", + "pacote": "^21.0.2" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "11.2.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^7.0.0", + "ci-info": "^4.0.0", + "npm-package-arg": "^13.0.0", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.7", + "sigstore": "^4.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "9.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "8.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^19.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "8.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^7.0.0", + "@npmcli/run-script": "^10.0.0", + "json-parse-even-better-errors": "^5.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "11.5.1", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "15.0.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/agent": "^4.0.0", + "@npmcli/redact": "^4.0.0", + "cacache": "^20.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^6.0.0", + "ssri": "^13.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "10.2.5", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.3", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "5.0.2", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^2.0.0", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + }, + "optionalDependencies": { + "iconv-lite": "^0.7.2" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.6", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minipass": "^7.1.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "2.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/negotiator": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "12.4.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "nopt": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "tar": "^7.5.4", + "tinyglobby": "^0.2.12", + "undici": "^6.25.0", + "which": "^6.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "9.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^4.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "7.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "8.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "5.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "13.0.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^9.0.0", + "proc-log": "^6.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^7.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "10.0.4", + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^8.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "11.0.3", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^8.0.0", + "npm-normalize-package-bin": "^5.0.0", + "npm-package-arg": "^13.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "12.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "19.1.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^4.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^15.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^5.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^13.0.0", + "proc-log": "^6.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "4.0.0", + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/pacote": { + "version": "21.5.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "@gar/promise-retry": "^1.0.0", + "@npmcli/git": "^7.0.0", + "@npmcli/installed-package-contents": "^4.0.0", + "@npmcli/package-json": "^7.0.0", + "@npmcli/promise-spawn": "^9.0.0", + "@npmcli/run-script": "^10.0.0", + "cacache": "^20.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^13.0.0", + "npm-packlist": "^10.0.1", + "npm-pick-manifest": "^11.0.1", + "npm-registry-fetch": "^19.0.0", + "proc-log": "^6.0.0", + "sigstore": "^4.0.0", + "ssri": "^13.0.0", + "tar": "^7.4.3" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^5.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "2.0.2", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/postcss-selector-parser": { + "version": "7.1.4", + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "6.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "3.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^5.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "5.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^3.0.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "6.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.8.4", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "4.1.1", + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^4.0.0", + "@sigstore/core": "^3.2.1", + "@sigstore/protobuf-specs": "^0.5.0", + "@sigstore/sign": "^4.1.1", + "@sigstore/tuf": "^4.0.2", + "@sigstore/verify": "^3.1.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.9", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.1.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.23", + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/ssri": { + "version": "13.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "10.2.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "7.5.16", + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.1.0", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.17", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.4", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "4.1.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "4.1.0", + "debug": "^4.4.3", + "make-fetch-happen": "^15.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/undici": { + "version": "6.26.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18.17" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "7.0.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "4.0.0", + "inBundle": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/npm/node_modules/which": { + "version": "6.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^4.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "7.0.1", + "inBundle": true, + "license": "ISC", + "dependencies": { + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "5.0.0", + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index df0b0ba..d6b90d8 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,9 @@ "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "driver.js": "^1.8.0", + "install": "^0.13.0", "next": "16.1.4", + "npm": "^11.17.0", "react": "^19.2.7", "react-dom": "19.2.7", "tailwind-merge": "^3.4.0" diff --git a/src/features/outages/components/Dry.tsx b/src/features/outages/components/Dry.tsx new file mode 100644 index 0000000..bb25a97 --- /dev/null +++ b/src/features/outages/components/Dry.tsx @@ -0,0 +1,219 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; + +import { getOutages } from "@/services/outages"; +import type { Outage } from "@/types/outages"; + +function getErrorMessage(error: unknown) { + return error instanceof Error ? error.message : "Failed to load outages"; +} + +const PRESETS_KEY = "outage_filter_presets"; + +export interface FilterPreset { + name: string; + severity?: string; + status?: string; +} + +export function useFilterPresets() { + const [presets, setPresets] = useState(() => { + try { + return JSON.parse(localStorage.getItem(PRESETS_KEY) ?? "[]") as FilterPreset[]; + } catch { + return []; + } + }); + + function savePreset(preset: FilterPreset) { + setPresets((prev) => { + const next = [...prev.filter((p) => p.name !== preset.name), preset]; + localStorage.setItem(PRESETS_KEY, JSON.stringify(next)); + return next; + }); + } + + function deletePreset(name: string) { + setPresets((prev) => { + const next = prev.filter((p) => p.name !== name); + localStorage.setItem(PRESETS_KEY, JSON.stringify(next)); + return next; + }); + } + + return { presets, savePreset, deletePreset }; +} + +export type SortField = "detected_at" | "severity" | "status"; +export type SortOrder = "asc" | "desc"; + +const VALID_SORT_FIELDS: SortField[] = ["detected_at", "severity", "status"]; +const VALID_SORT_ORDERS: SortOrder[] = ["asc", "desc"]; + +function parseSortField(v: string | null): SortField | undefined { + return VALID_SORT_FIELDS.includes(v as SortField) ? (v as SortField) : undefined; +} + +function parseSortOrder(v: string | null): SortOrder { + return VALID_SORT_ORDERS.includes(v as SortOrder) ? (v as SortOrder) : "desc"; +} + +// Existing state manager — extended with search + sort + full URL sync (FE-058, FE-059, FE-060) +export function useOutagesTableState() { + const params = useSearchParams(); + const router = useRouter(); + + const page = Math.max(1, Number(params?.get("page") ?? 1)); + const pageSize = Number(params?.get("page_size") ?? 10); + const severity = params?.get("severity") ?? undefined; + const status = params?.get("status") ?? undefined; + // FE-058: search query + const search = params?.get("search") ?? undefined; + // FE-059: sort field + order + const sortField = parseSortField(params?.get("sort_field") ?? null); + const sortOrder = parseSortOrder(params?.get("sort_order") ?? null); + + function setParam(key: string, value?: string) { + const next = new URLSearchParams(params?.toString() ?? ""); + if (value) { + next.set(key, value); + } else { + next.delete(key); + } + router.push(`?${next.toString()}`); + } + + function setMultiParam(updates: Record) { + const next = new URLSearchParams(params?.toString() ?? ""); + for (const [key, value] of Object.entries(updates)) { + if (value) { + next.set(key, value); + } else { + next.delete(key); + } + } + router.push(`?${next.toString()}`); + } + + function setPage(nextPage: number) { + setParam("page", String(Math.max(1, nextPage))); + } + + function setPageSize(nextPageSize: number) { + setMultiParam({ page_size: String(nextPageSize), page: "1" }); + } + + function setSeverity(nextSeverity?: string) { + setMultiParam({ severity: nextSeverity, page: "1" }); + } + + function setStatus(nextStatus?: string) { + setMultiParam({ status: nextStatus, page: "1" }); + } + + // FE-058 + function setSearch(nextSearch?: string) { + setMultiParam({ search: nextSearch || undefined, page: "1" }); + } + + // FE-059 + function setSort(field: SortField, order: SortOrder) { + setMultiParam({ sort_field: field, sort_order: order, page: "1" }); + } + + function clearSort() { + setMultiParam({ sort_field: undefined, sort_order: undefined, page: "1" }); + } + + return { + state: { + page, + page_size: pageSize, + severity, + status, + search, + sort_field: sortField, + sort_order: sortOrder, + }, + actions: { + setParam, + setPage, + setPageSize, + setSeverity, + setStatus, + setSearch, + setSort, + clearSort, + }, + }; +} + +// New data fetching & polling hook for the Outages list +export function useOutagesList( + page: number, + severity?: string, + status?: string, + pageSize: number = 10, + search?: string, + sortField?: SortField, + sortOrder?: SortOrder, +) { + const [outages, setOutages] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const isFetching = useRef(false); + const hasOutagesRef = useRef(false); + + useEffect(() => { + hasOutagesRef.current = outages.length > 0; + }, [outages]); + + useEffect(() => { + let isMounted = true; + setLoading(true); + + const fetchList = async () => { + if (isFetching.current) return; + isFetching.current = true; + + try { + const data = await getOutages({ + page, + page_size: pageSize, + severity, + status, + search, + sort_field: sortField, + sort_order: sortOrder, + }); + if (isMounted) { + setOutages(data.items); + setError(null); + } + } catch (error: unknown) { + if (isMounted && !hasOutagesRef.current) { + setError(getErrorMessage(error)); + } + } finally { + isFetching.current = false; + if (isMounted) setLoading(false); + } + }; + + fetchList(); + + // The list page constantly polls every 15 seconds to ensure we + // catch any newly generated incidents as well as updates to existing ones. + const intervalId = setInterval(fetchList, 15000); + + return () => { + isMounted = false; + clearInterval(intervalId); + }; + }, [page, pageSize, severity, status, search, sortField, sortOrder]); + + return { outages, loading, error }; +} diff --git a/src/lib/outages.ts b/src/lib/outages.ts index a6d544f..cb91db1 100644 --- a/src/lib/outages.ts +++ b/src/lib/outages.ts @@ -40,7 +40,7 @@ export async function fetchOutages( options?: { signal?: AbortSignal }, ): Promise { const { data } = await api.get("/outages", { - ...(options?.signal ? { signal: options.signal } : {}), + signal: options?.signal, params: { page: query.page, page_size: query.page_size ?? DEFAULT_OUTAGES_PAGE_SIZE, diff --git a/src/lib/session-sse.ts b/src/lib/session-sse.ts index 5576eec..d03b971 100644 --- a/src/lib/session-sse.ts +++ b/src/lib/session-sse.ts @@ -34,7 +34,7 @@ export interface SessionSseEvent { export function parseSessionSseFrame(frame: string): SessionSseEvent | null { const fields = new Map(); - for (const line of frame.replaceAll("\r\n", "\n").split("\n")) { + for (const line of frame.replaceAll("\\r\\n", "\\n").split("\\n")) { if (!line || line.startsWith(":")) continue; const separator = line.indexOf(":"); const field = separator === -1 ? line : line.slice(0, separator); @@ -44,7 +44,7 @@ export function parseSessionSseFrame(frame: string): SessionSseEvent | null { } const eventType = fields.get("event")?.at(-1); - const dataStr = fields.get("data")?.join("\n"); + const dataStr = fields.get("data")?.join("\\n"); if (eventType !== "session_revoked" || !dataStr) return null; try { @@ -119,16 +119,18 @@ export function connectSessionSse( if (response.status === 401 || response.status === 403) { closed = true; onEvent({ type: "session_revoked", reason: "session_expired" }); - shouldRetry = false; - } else { - console.warn( - `[session-sse] /auth/events returned ${response.status} — retrying`, - ); + return; } - } else { - // Reset retry count on successful connection - retryCount = 0; + console.warn( + `[session-sse] /auth/events returned ${response.status} — ${response.status === 401 || response.status === 403 ? "stopping" : "retrying"}`, + ); + return; + } + + // Reset retry count on successful connection + retryCount = 0; + if (shouldRetry) { const reader = response.body?.getReader(); if (!reader) { console.warn("[session-sse] Response body has no readable stream"); @@ -153,9 +155,9 @@ export function connectSessionSse( } } } - } catch { - // Network/abort errors fall through to the reconnect check below; - // it no-ops when the connection was closed or aborted. + } catch (err) { + // If aborted/closed, don't reconnect + if (closed) return; } // Reconnect with backoff if not closed diff --git a/tests/auth-flow.test.tsx b/tests/auth-flow.test.tsx index f089440..c256824 100644 --- a/tests/auth-flow.test.tsx +++ b/tests/auth-flow.test.tsx @@ -152,10 +152,6 @@ describe("useSession", () => { it("is unauthenticated when no token or past session exists", async () => { mockGetAccessToken.mockReturnValue(null); - // The provider always probes the cookie-only /auth/session endpoint - // (a missing local flag does not rule out valid httpOnly cookies); - // a 401 means definitively no session. - mockHttp({ sessionStatus: 401 }); const { result } = renderSessionHook(); @@ -165,17 +161,13 @@ describe("useSession", () => { expect(result.current.user).toBeNull(); - expect(mockGet).toHaveBeenCalledWith( - "/auth/session", - expect.any(Object), - ); + expect(mockGet).not.toHaveBeenCalled(); }); }); describe("storeSession", () => { it("stores session and updates authenticated state", async () => { mockGetAccessToken.mockReturnValue(null); - mockHttp({ sessionStatus: 401 }); const { result } = renderSessionHook(); @@ -268,8 +260,6 @@ describe("useSession", () => { it("does not call clearTokens unnecessarily", async () => { mockGetAccessToken.mockReturnValue(null); - // A valid cookie session means no tokens need clearing. - mockHttp({ sessionStatus: 200 }); renderSessionHook(); diff --git a/tests/contract.test.ts b/tests/contract.test.ts index 3b8256d..dc2c363 100644 --- a/tests/contract.test.ts +++ b/tests/contract.test.ts @@ -1,18 +1,9 @@ /** ApexChain Network Operations Intelligence Platform */ import { describe, it, expect } from "vitest"; -const API_BASE = process.env.API_BASE_URL || "http://localhost:4010/api/v1"; +describe("API Contract Tests", () => { + const API_BASE = process.env.API_BASE_URL || "http://localhost:4010/api/v1"; -// Contract tests exercise a live API (the Prism contract mock on :4010 or a -// real backend). Skip — rather than fail — when it isn't running, so the -// default `npm test` suite stays green without infrastructure. -const apiReachable = await fetch(`${API_BASE}/health`, { - signal: AbortSignal.timeout(1_500), -}) - .then((response) => response.ok) - .catch(() => false); - -describe.skipIf(!apiReachable)("API Contract Tests", () => { it("GET /health returns 200", async () => { const response = await fetch(`${API_BASE}/health`); expect(response.status).toBe(200); diff --git a/tests/payment-detail-drawer.test.tsx b/tests/payment-detail-drawer.test.tsx index 0538a56..e635047 100644 --- a/tests/payment-detail-drawer.test.tsx +++ b/tests/payment-detail-drawer.test.tsx @@ -1,6 +1,6 @@ /** ApexChain Network Operations Intelligence Platform */ import { describe, it, expect, vi, beforeEach } from "vitest"; -import { render, screen, fireEvent } from "@testing-library/react"; +import { render, screen, fireEvent, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { PaymentDetailDrawer } from "@/components/payments/payment-detail-drawer"; @@ -10,12 +10,6 @@ vi.mock("@/services/paymentService", () => ({ reconcilePayment: vi.fn(), })); -// The drawer calls useToast() unconditionally, which throws outside a -// ToastProvider — mock it like the other component tests do. -vi.mock("@/components/ui/toast", () => ({ - useToast: () => vi.fn(), -})); - describe("PaymentDetailDrawer", () => { const mockOnClose = vi.fn(); @@ -32,7 +26,7 @@ describe("PaymentDetailDrawer", () => { const user = userEvent.setup(); render(); - const closeButton = screen.getByLabelText("Close drawer"); + const closeButton = screen.getByLabelText("Close"); await user.click(closeButton); expect(mockOnClose).toHaveBeenCalled(); diff --git a/tests/payments-view.test.tsx b/tests/payments-view.test.tsx index eff43c1..639a572 100644 --- a/tests/payments-view.test.tsx +++ b/tests/payments-view.test.tsx @@ -68,15 +68,13 @@ describe("PaymentsView", () => { it("shows empty state", async () => { mockFetchPayments.mockResolvedValue({ items: [], total: 0 }); render(); - // RouteEmptyState repeats the title in a polite live region, so query - // the visible heading instead of any text node. - expect(await screen.findByRole("heading", { name: "No payments found" })).toBeInTheDocument(); + expect(await screen.findByText("No payments found")).toBeInTheDocument(); }); it("shows error state on failure", async () => { mockFetchPayments.mockRejectedValue(new Error("fail")); render(); - expect(await screen.findByRole("heading", { name: "Payments unavailable" })).toBeInTheDocument(); + expect(await screen.findByText("Payments unavailable")).toBeInTheDocument(); }); }); diff --git a/tests/session-sse.test.ts b/tests/session-sse.test.ts index 9d7c4c3..2775496 100644 --- a/tests/session-sse.test.ts +++ b/tests/session-sse.test.ts @@ -1,66 +1,79 @@ /** ApexChain Network Operations Intelligence Platform */ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { - connectSessionSse, - parseSessionSseFrame, - type SessionRevokeReason, -} from "@/lib/session-sse"; - -describe("parseSessionSseFrame", () => { - it("parses a single-line session_revoked frame", () => { - expect( - parseSessionSseFrame( - 'event: session_revoked\ndata: {"reason":"admin_logout"}\n\n', - ), - ).toEqual({ type: "session_revoked", reason: "admin_logout" }); +import { connectSessionSse, processFrame } from "@/lib/session-sse"; + +describe("processFrame", () => { + const onEvent = vi.fn(); + + beforeEach(() => { + onEvent.mockClear(); }); - it("joins multi-line data fields before JSON-parsing (SSE spec)", () => { - expect( - parseSessionSseFrame( - 'event: session_revoked\ndata: {"reason":\ndata: "admin_logout"}\n\n', - ), - ).toEqual({ type: "session_revoked", reason: "admin_logout" }); + afterEach(() => { + vi.restoreAllMocks(); }); - it("returns null when multi-line data does not form valid JSON", () => { - expect( - parseSessionSseFrame( - 'event: session_revoked\ndata: {"reason":"admin_logout"}\ndata: {"reason":"password_changed"}\n\n', - ), - ).toBeNull(); + it("handles a single-line session_revoked frame", () => { + processFrame( + 'event: session_revoked\ndata: {"reason":"admin_logout"}\n\n', + onEvent, + ); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + type: "session_revoked", + reason: "admin_logout", + }); + }); + + it("keeps the current multi-line data truncation behavior", () => { + processFrame( + 'event: session_revoked\ndata: {"reason":"admin_logout"}\ndata: {"reason":"password_changed"}\n\n', + onEvent, + ); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + type: "session_revoked", + reason: "password_changed", + }); }); it("ignores malformed JSON payloads", () => { - expect( - parseSessionSseFrame('event: session_revoked\ndata: {"reason":\n\n'), - ).toBeNull(); + processFrame('event: session_revoked\ndata: {"reason":\n\n', onEvent); + + expect(onEvent).not.toHaveBeenCalled(); }); it("ignores unknown event types", () => { - expect( - parseSessionSseFrame('event: heartbeat\ndata: {"timestamp":1234567890}\n\n'), - ).toBeNull(); + processFrame('event: heartbeat\ndata: {"timestamp":1234567890}\n\n', onEvent); + + expect(onEvent).not.toHaveBeenCalled(); }); it("ignores a session_revoked payload when the event line is missing", () => { - expect( - parseSessionSseFrame('data: {"reason":"session_expired"}\n\n'), - ).toBeNull(); + processFrame('data: {"reason":"session_expired"}\n\n', onEvent); + + expect(onEvent).not.toHaveBeenCalled(); }); - it.each<[string, SessionRevokeReason]>([ + it.each([ ["admin_logout", "admin_logout"], ["password_changed", "password_changed"], ["session_expired", "session_expired"], ["not_a_known_reason", "unknown"], ])("maps reason %s to %s", (reason, expected) => { - expect( - parseSessionSseFrame( - `event: session_revoked\ndata: {"reason":"${reason}"}\n\n`, - ), - ).toEqual({ type: "session_revoked", reason: expected }); + processFrame( + `event: session_revoked\ndata: {"reason":"${reason}"}\n\n`, + onEvent, + ); + + expect(onEvent).toHaveBeenCalledTimes(1); + expect(onEvent).toHaveBeenCalledWith({ + type: "session_revoked", + reason: expected, + }); }); }); @@ -73,43 +86,43 @@ describe("connectSessionSse reconnect behavior", () => { vi.runOnlyPendingTimers(); vi.useRealTimers(); vi.unstubAllGlobals(); - vi.restoreAllMocks(); }); it("schedules a retry after a non-200 response", async () => { - const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 503 }); + const fetchMock = vi.fn().mockResolvedValue({ + ok: false, + status: 503, + }); vi.stubGlobal("fetch", fetchMock); vi.spyOn(Math, "random").mockReturnValue(0.5); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); const connection = connectSessionSse(() => {}); - // Flush the connect() microtask chain (fetch resolution) without - // advancing the scheduled retry timer itself. - await vi.advanceTimersByTimeAsync(0); + await Promise.resolve(); expect(fetchMock).toHaveBeenCalledTimes(1); expect(setTimeoutSpy).toHaveBeenCalledTimes(1); + expect(setTimeoutSpy).toHaveBeenCalledWith(expect.any(Function), expect.any(Number)); connection.close(); }); it("stops retrying after 401 or 403 responses", async () => { - const fetchMock = vi.fn().mockResolvedValue({ ok: false, status: 401 }); + const fetchMock = vi.fn().mockResolvedValue({ + ok: false, + status: 401, + }); vi.stubGlobal("fetch", fetchMock); - const onEvent = vi.fn(); const setTimeoutSpy = vi.spyOn(globalThis, "setTimeout"); - const connection = connectSessionSse(onEvent); + const connection = connectSessionSse(() => {}); + await Promise.resolve(); await vi.advanceTimersByTimeAsync(1_000); expect(fetchMock).toHaveBeenCalledTimes(1); expect(setTimeoutSpy).not.toHaveBeenCalled(); - expect(onEvent).toHaveBeenCalledWith({ - type: "session_revoked", - reason: "session_expired", - }); connection.close(); }); diff --git a/tests/settings-theme.test.tsx b/tests/settings-theme.test.tsx index 9078454..fb21665 100644 --- a/tests/settings-theme.test.tsx +++ b/tests/settings-theme.test.tsx @@ -83,7 +83,7 @@ describe("SettingsPage theme effect", () => { const changeRemovals = removeEventListener.mock.calls.filter(([type]) => type === "change"); expect(changeRemovals).toHaveLength(1); - expect(changeRemovals[0]![1]).toBe(addedHandler); + expect(changeRemovals[0][1]).toBe(addedHandler); }); it("keeps add/remove balanced across re-renders (no growing leak)", () => { diff --git a/tests/settings-wallet.test.tsx b/tests/settings-wallet.test.tsx index 4a50949..df5083e 100644 --- a/tests/settings-wallet.test.tsx +++ b/tests/settings-wallet.test.tsx @@ -4,16 +4,6 @@ import { fireEvent, render, screen } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import SettingsPage from "@/app/setting/page"; -import { I18nProvider } from "@/i18n/i18n"; - -// SettingsPage requires the i18n context, same as settings-theme.test.tsx. -function renderSettingsPage() { - return render( - - - , - ); -} const mockGet = vi.fn(); const mockPost = vi.fn(); @@ -25,18 +15,13 @@ vi.mock("@/lib/api", () => ({ clearTokens: vi.fn(), setTokens: vi.fn(), })); -vi.mock("@/lib/explorer", () => ({ explorerLink: () => null, STELLAR_NETWORK: "testnet" })); +vi.mock("@/lib/explorer", () => ({ explorerLink: () => null })); vi.mock("@/hooks/useSession", () => ({ useSession: () => ({ state: "unauthenticated", user: null }), })); vi.mock("@/components/ui/toast", () => ({ useToast: () => mockToast, })); -// SettingsPage calls useRouter() for session actions; outside a real Next.js -// App Router tree this throws unless mocked (same as the other page tests). -vi.mock("next/navigation", () => ({ - useRouter: () => ({ push: vi.fn(), replace: vi.fn(), back: vi.fn() }), -})); const wallet = { user_id: "u1", public_key: "GABC", funded: true, trustline_ready: true, active: true, created_at: "2026-01-01T00:00:00Z", last_updated: "2026-01-01T00:00:00Z" }; const walletStatus = { user_id: "u1", public_key: "GABC", funded: true, trustline_ready: true, usable: true, active: true, last_updated: "2026-01-01T00:00:00Z" }; @@ -45,15 +30,15 @@ describe("SettingsPage", () => { beforeEach(() => { mockGet.mockReset(); mockPost.mockReset(); }); it("renders with unauthenticated state and no wallet", () => { - renderSettingsPage(); + render(); expect(screen.getByText("Settings and Wallet Control")).toBeInTheDocument(); - expect(screen.getAllByText("Not signed in.").length).toBeGreaterThan(0); + expect(screen.getByText("Not signed in.")).toBeInTheDocument(); expect(screen.getByText("Not linked")).toBeInTheDocument(); }); it("loads and displays wallet details", async () => { mockGet.mockResolvedValueOnce({ data: wallet }).mockResolvedValueOnce({ data: walletStatus }); - renderSettingsPage(); + render(); fireEvent.change(screen.getByPlaceholderText("User ID"), { target: { value: "u1" } }); fireEvent.click(screen.getByRole("button", { name: /load wallet details/i })); expect(await screen.findByText("Wallet details loaded.")).toBeInTheDocument(); @@ -61,7 +46,7 @@ describe("SettingsPage", () => { }); it("renders unconfigured state when NEXT_PUBLIC_SLA_CONTRACT_ID is unset", () => { - renderSettingsPage(); + render(); // The SLA contract card renders an explicit unconfigured state instead of // surfacing the all-C placeholder as if it were a real Stellar contract. @@ -75,14 +60,14 @@ describe("SettingsPage", () => { }); it("shows error when loading wallet without user id", async () => { - renderSettingsPage(); + render(); fireEvent.click(screen.getByRole("button", { name: /load wallet details/i })); expect(await screen.findByText("Provide a user ID or log in before loading wallet details.")).toBeInTheDocument(); }); it("shows ready banner when wallet is usable", async () => { mockGet.mockResolvedValueOnce({ data: wallet }).mockResolvedValueOnce({ data: walletStatus }); - renderSettingsPage(); + render(); fireEvent.change(screen.getByPlaceholderText("User ID"), { target: { value: "u1" } }); fireEvent.click(screen.getByRole("button", { name: /load wallet details/i })); expect(await screen.findByText(/Wallet is fully ready/)).toBeInTheDocument(); @@ -90,7 +75,7 @@ describe("SettingsPage", () => { it("shows not-ready guidance when wallet is unusable", async () => { mockGet.mockResolvedValueOnce({ data: wallet }).mockResolvedValueOnce({ data: { ...walletStatus, usable: false, funded: false } }); - renderSettingsPage(); + render(); fireEvent.change(screen.getByPlaceholderText("User ID"), { target: { value: "u1" } }); fireEvent.click(screen.getByRole("button", { name: /load wallet details/i })); expect(await screen.findByText("Wallet Not Ready — Next Steps")).toBeInTheDocument(); @@ -104,7 +89,7 @@ describe("SettingsPage", () => { .mockResolvedValueOnce({ data: walletStatus }) .mockResolvedValueOnce({ data: { address: "GABC", balances: {}, last_updated: "2026-01-01T00:00:00Z" } }); - renderSettingsPage(); + render(); fireEvent.change(screen.getByPlaceholderText("User ID"), { target: { value: "u1" } }); fireEvent.click(screen.getByRole("button", { name: /load wallet details/i })); expect(await screen.findByText("Wallet details loaded.")).toBeInTheDocument(); diff --git a/tests/useOutagesTableState.test.ts b/tests/useOutagesTableState.test.ts index 8598177..0d268a8 100644 --- a/tests/useOutagesTableState.test.ts +++ b/tests/useOutagesTableState.test.ts @@ -1,15 +1,11 @@ /** ApexChain Network Operations Intelligence Platform */ -import { describe, it, expect, beforeEach } from "vitest"; +import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderHook, act } from "@testing-library/react"; import { useFilterPresets } from "@/hooks/useOutagesTableState"; -import { resetPreferences } from "@/lib/preferences"; describe("useFilterPresets", () => { beforeEach(() => { localStorage.clear(); - // The preferences module caches state at module level; reset it between - // tests so presets from one test don't leak into the next. - resetPreferences(); }); it("initializes with empty presets", () => { @@ -19,11 +15,7 @@ describe("useFilterPresets", () => { it("loads presets from localStorage", () => { const presets = [{ name: "High Severity", severity: "high" }]; - // Presets live inside the synced preferences object (see src/lib/preferences.ts). - localStorage.setItem( - "apexchain_user_preferences", - JSON.stringify({ outageFilterPresets: presets }), - ); + localStorage.setItem("outage_filter_presets", JSON.stringify(presets)); const { result } = renderHook(() => useFilterPresets()); expect(result.current.presets).toEqual(presets); diff --git a/vitest.setup.ts b/vitest.setup.ts index aa5aec3..f149f27 100644 --- a/vitest.setup.ts +++ b/vitest.setup.ts @@ -1,21 +1 @@ import "@testing-library/jest-dom/vitest"; -import { vi } from "vitest"; - -// jsdom does not implement matchMedia. Provide a safe default so components -// touching theme logic render; tests that assert listener behavior override -// this with vi.stubGlobal (see tests/settings-theme.test.tsx). -if (typeof window !== "undefined" && !window.matchMedia) { - Object.defineProperty(window, "matchMedia", { - writable: true, - value: vi.fn().mockImplementation((query: string) => ({ - matches: false, - media: query, - onchange: null, - addEventListener: vi.fn(), - removeEventListener: vi.fn(), - addListener: vi.fn(), // deprecated, some libs still call it - removeListener: vi.fn(), - dispatchEvent: vi.fn(), - })), - }); -}