diff --git a/package-lock.json b/package-lock.json index 91bc650..7dbc63a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -414,9 +414,9 @@ } }, "node_modules/@csstools/css-color-parser": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.7.tgz", - "integrity": "sha512-CmjJFQTFQx/U/xNJhSjCQ0ilpesPmNQ8+eOUeM/+kDOVW33qsIjeOXc27vrQDdWVkf83ZSWwtg7kXSUvKDJ8cQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.8.tgz", + "integrity": "sha512-3chWb7PRLijpJpPIKkDxdu6IBeO5MrFACND57On0j8OPpc0wZibcGc3xAHrSEbOx/KDRyMHoIxGn0w1PhXMYHw==", "dev": true, "funding": [ { @@ -1535,13 +1535,13 @@ } }, "node_modules/@playwright/test": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.60.0.tgz", - "integrity": "sha512-O71yZIbAh/PxDMNGns37GHBIfrVkEVyn+AXyIa5dOTfb4/xNvRWV+Vv/NMbNCtODB/pO7vLlF2OTmMVLhmr7Ag==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.61.0.tgz", + "integrity": "sha512-cKA5B6lpFEMyMGjxF54QihfYpB4FkEGH+qZhtArDEG+wezQAJY8Pq6C7T1SjWz+FFzt3TbyoXBQYk/0292TdJA==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright": "1.60.0" + "playwright": "1.61.0" }, "bin": { "playwright": "cli.js" @@ -1853,6 +1853,7 @@ "version": "15.0.0", "resolved": "https://registry.npmjs.org/@stellar/stellar-base/-/stellar-base-15.0.0.tgz", "integrity": "sha512-XQhxUr9BYiEcFcgc4oWcCMR9QJCny/GmmGsuwPKf/ieIcOeb5149KLHYx9mJCA0ea8QbucR2/GzV58QbXOTxQA==", + "deprecated": "This package is now rolled into @stellar/stellar-sdk. Please use @stellar/stellar-sdk to continue receiving updates and support.", "license": "Apache-2.0", "dependencies": { "@noble/curves": "^1.9.7", @@ -2208,17 +2209,6 @@ "node": ">=18" } }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", - "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", - "dev": true, - "license": "Apache-2.0", - "peer": true, - "dependencies": { - "dequal": "^2.0.3" - } - }, "node_modules/@testing-library/jest-dom": { "version": "6.9.1", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-6.9.1.tgz", @@ -2380,17 +2370,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.0.tgz", - "integrity": "sha512-bFNvl9ZczlVb+wR2Akszf3gHfKVj/8WanXaGJ3UstTA7brNKg0cNdk6X1Psu5V7MZ2oQtzZKOEzIUehaoxbDGw==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.61.1.tgz", + "integrity": "sha512-ZPlVl3PB3et/59Ne0fv/sci6ZXz4T4Hp4nTJ56i/Y0gR89ARb+KphojTq6j+56E5PIezmOIOOWyY+aWQFd+IkQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/type-utils": "8.61.0", - "@typescript-eslint/utils": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/type-utils": "8.61.1", + "@typescript-eslint/utils": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -2403,7 +2393,7 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.61.0", + "@typescript-eslint/parser": "^8.61.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } @@ -2419,16 +2409,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.0.tgz", - "integrity": "sha512-5B7PfA2e1NQGCnDHd/0lW7W3gvp3d59Ryw54FYO8Uswxo9f6ikw3AZV+Xj/TvpImmpsiYyUqAfhC6kJID1jF6w==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.61.1.tgz", + "integrity": "sha512-PJ5vePq5/ognBbrIcoC5+SHO5dfpeLPzP9FpLkzWrguoYQEeeSjlJpVwOpo1JRSTEi7dRcwNy4h4dzV70PqHcg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3" }, "engines": { @@ -2444,14 +2434,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.0.tgz", - "integrity": "sha512-DV42F7MLJO6Rax7SK1yg43tcnEfGUrurSpSxKuVX+a3RCTzBlH3fuxprrOJXKCJGAaw82xXocikJ0uQaqwXgGA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.61.1.tgz", + "integrity": "sha512-PrC4JYGmR241lYnfhmKGTXkFqv8+ymbTFgSAY0fVXpY82/QkMw5TZPl+vGzuDDU2QYJk9fIDOBTntF+yDv9LEA==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.61.0", - "@typescript-eslint/types": "^8.61.0", + "@typescript-eslint/tsconfig-utils": "^8.61.1", + "@typescript-eslint/types": "^8.61.1", "debug": "^4.4.3" }, "engines": { @@ -2466,14 +2456,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.0.tgz", - "integrity": "sha512-IWdXFHFSb6mlC3HPc7QsLDm5zYEbUla6trDEHf32D3/dnuUyXd87plScSNXSbm0/RxMvObpI17sv/EDTGrGZkA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.61.1.tgz", + "integrity": "sha512-L2bdIeoQS8FlKAvONAr20w6OcLXeB+qiDKbAooS9A0Ben+iSIkBef0FxqwKWYqt5sa0i4KJtxVyVmhMylKzF5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0" + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2484,9 +2474,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.0.tgz", - "integrity": "sha512-O5Amvdv9ztMpxpf+vmFULGG78IE6Qwdr3bCGvqwG4nwc9H2qXkOYJJnRbRHyMkQTjv1d03olqwwwzHLMqpFePQ==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.61.1.tgz", + "integrity": "sha512-UN/H4di+OO7EWx2ovME+8t31YO+KVnK0RRKEHR3kOt21/Ay8BOq3M1OMvWs5vNiqcFCYGYoxK3MXPZzmMUE+yg==", "dev": true, "license": "MIT", "engines": { @@ -2501,15 +2491,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.0.tgz", - "integrity": "sha512-TuBiQYIkd97yBfInHCTKVYMbX4kvEmpOEuixIuzCU9p8BGT1SfyyO0d0IfDMbPIHcjn/hWnusUX5e8v5Xg+X8A==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.61.1.tgz", + "integrity": "sha512-GYRicKmVK0C4fsKgaACaknOUAq9Oa2kwsjnpFhFcS/5p4Ht5IP9OVLbgIgcK4SRk92nVHFluurg1lumD9dBcLw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -2526,9 +2516,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.0.tgz", - "integrity": "sha512-9QTQpZ5Iin4CdIodfbDQFSeiSJKidgYJYug1P9CC2xWgUTvlmixViqDZNciMjwLBZyJnG4tGmPl97rVAFb1AJg==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.61.1.tgz", + "integrity": "sha512-G+CRlPqLv7Bz1IZVs03x5K59F1veqL0EJUROAdGhKsEq8qOiRiZbI+HUojPq5l0fEGOKModD9br6lObhB8zkoA==", "dev": true, "license": "MIT", "engines": { @@ -2540,16 +2530,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.0.tgz", - "integrity": "sha512-42zatd5qSvvcV1JdDBCLxYRznvP4eIHpPoZXdkPFnAmanA4FuZ5dibSnCBggY8hQnqajPpoGjXFdZ7fIJKQnlA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.61.1.tgz", + "integrity": "sha512-u+oQD3BqYWPc8YV9Zab4vaJElJuwOLPRc10Jm1o/qS+6Qwen14HCWwx0Seo4LnSn2wxea2Ik8DxPt2/FHmuhrg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.61.0", - "@typescript-eslint/tsconfig-utils": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/visitor-keys": "8.61.0", + "@typescript-eslint/project-service": "8.61.1", + "@typescript-eslint/tsconfig-utils": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/visitor-keys": "8.61.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -2607,9 +2597,9 @@ } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -2620,16 +2610,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.0.tgz", - "integrity": "sha512-3bzFt7ImFMW/jVYwJamDoe/dMOdFLSC6pom6rRjdh4SZJEYupyMzem8e7vKZLclLfpHjlwSAXOUxtKxGXUiLqA==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.61.1.tgz", + "integrity": "sha512-1+P/3Dj6jvtybE1q0HQ6yBt/gq+oKJyLdEv4HdnqasaEXRSYCAsD59mXEVQnM/ULNdQxbX77tdG4jPRjIS6knA==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.61.0", - "@typescript-eslint/types": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0" + "@typescript-eslint/scope-manager": "8.61.1", + "@typescript-eslint/types": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -2644,13 +2634,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.0.tgz", - "integrity": "sha512-QVLZu3ZPQEE+HICQyAMZ2yLQhxf0meY/wx6Hx14YcTNj13JB3qHlX3lJ02L3fLGHgERRH71kvYDwiXIguT3AjQ==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.61.1.tgz", + "integrity": "sha512-6fJ9MHWtK14C1DSkiMlHUSOmrVebL7150xZJBlJiL62jjhIA4JmOq6flwBgDxIdBKKdoiZRel+dfPD5MLfny3w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.61.0", + "@typescript-eslint/types": "8.61.1", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -3209,13 +3199,13 @@ "license": "Python-2.0" }, "node_modules/aria-query": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", - "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", "dev": true, "license": "Apache-2.0", - "engines": { - "node": ">= 0.4" + "dependencies": { + "dequal": "^2.0.3" } }, "node_modules/array-buffer-byte-length": { @@ -3494,9 +3484,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.10.37", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.37.tgz", - "integrity": "sha512-girxaJ7WZssDOFhzCGZTDKoTa1gk6A1TbflaYTpykLJ4UU9Fz9kx1aREM8JCuoVHbL8X8T/mJg7w2oYSq72Oig==", + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", "license": "Apache-2.0", "bin": { "baseline-browser-mapping": "dist/cli.cjs" @@ -3516,9 +3506,9 @@ } }, "node_modules/bignumber.js": { - "version": "11.1.3", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.3.tgz", - "integrity": "sha512-+esZiNSo6VgFokTsYX6mYqNJfFd/IczzZCd4Z7cR8e+AQWhvIcj6nqQ1h9814D9u/TApU0jjTVmfWL0Pd1ZBdA==", + "version": "11.1.4", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-11.1.4.tgz", + "integrity": "sha512-AJ9dSeaUGj2xu7tEwmdqb51dqdb633xo4njI9K8ZFfcLrNr0XN8/EPkkZUNaF9fkCblGt2zVwZymesUdGynEkQ==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -3995,7 +3985,6 @@ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=6" } @@ -4052,9 +4041,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.372", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.372.tgz", - "integrity": "sha512-M3yhbAlilnwqC8D21t28UCDGHyitShTmmLRU/H+b74P6Ski16Nb9HONYEaVpMj/pwC7BEo5B95FpjODLCWbtfA==", + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", "dev": true, "license": "ISC" }, @@ -4161,6 +4150,25 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/es-abstract-get": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/es-abstract-get/-/es-abstract-get-1.0.0.tgz", + "integrity": "sha512-6PMWXpdhshVvFp+FoWYs1EvG1Nj0tvk0dZM+XcK0xMEM1czRVcP6ohqPWHy6qPagSpC8j4+p89WXlT+xXJs/fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.2", + "is-callable": "^1.2.7", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/es-define-property": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", @@ -4255,15 +4263,17 @@ } }, "node_modules/es-to-primitive": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", - "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.1.tgz", + "integrity": "sha512-CxN9N56HYfd2m/acc/NOFrZQsN9kU4eh+2kk6A707Kz1krH8tKmfrs5RnftB8WNX80T0NS7vSQsDOlg23diR2g==", "dev": true, "license": "MIT", "dependencies": { + "es-abstract-get": "^1.0.0", + "es-errors": "^1.3.0", "is-callable": "^1.2.7", - "is-date-object": "^1.0.5", - "is-symbol": "^1.0.4" + "is-date-object": "^1.1.0", + "is-symbol": "^1.1.1" }, "engines": { "node": ">= 0.4" @@ -4554,6 +4564,16 @@ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, + "node_modules/eslint-plugin-jsx-a11y/node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/eslint-plugin-react": { "version": "7.37.5", "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", @@ -4919,6 +4939,7 @@ "version": "2.3.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, "hasInstallScript": true, "license": "MIT", "optional": true, @@ -5427,9 +5448,9 @@ } }, "node_modules/is-bun-module/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "dev": true, "license": "ISC", "bin": { @@ -6461,9 +6482,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.13", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.13.tgz", + "integrity": "sha512-sPdqC6ByMVVGvF1ynvvMo0/o+oD1VX7DaHhijt1bFgjvBkHBib4t49GoNDhf2NDta4oeUNlaGbSt5K7qjZ955Q==", "funding": [ { "type": "github", @@ -6602,9 +6623,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.47", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.47.tgz", - "integrity": "sha512-Uzmd6LXpouKo8EUK68IjH4+E01w/hXyV3R3g/geCJo+rXLNfh1xucB+LOzYEOQPSiUK3h/xZf0cQGcSsmyL2Og==", + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", "dev": true, "license": "MIT", "engines": { @@ -6904,13 +6925,13 @@ } }, "node_modules/playwright": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", - "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.0.tgz", + "integrity": "sha512-Z+7BeeqQPRRzklHsVFP4KTGIyMxKUmfeRA4WisM6G3/XW6nwGeX6fX9qYaDa+CiUqpOkb2f6X3nar05R3kSuJQ==", "devOptional": true, "license": "Apache-2.0", "dependencies": { - "playwright-core": "1.60.0" + "playwright-core": "1.61.0" }, "bin": { "playwright": "cli.js" @@ -6923,9 +6944,9 @@ } }, "node_modules/playwright-core": { - "version": "1.60.0", - "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz", - "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==", + "version": "1.61.0", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.0.tgz", + "integrity": "sha512-caX7TrY3Ml6egyDX0WUcTHDxodl/b51y5wJOdCEA36QviK/s2g081hvmGs8eaE3DWb6NYZQ6BjO/QkNRPenoPA==", "devOptional": true, "license": "Apache-2.0", "bin": { @@ -7022,14 +7043,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/pretty-format/node_modules/react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true, - "license": "MIT", - "peer": true - }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -7042,6 +7055,13 @@ "react-is": "^16.13.1" } }, + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" + }, "node_modules/proxy-from-env": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-2.1.0.tgz", @@ -7130,11 +7150,12 @@ } }, "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "version": "17.0.2", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", + "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", "dev": true, - "license": "MIT" + "license": "MIT", + "peer": true }, "node_modules/redent": { "version": "3.0.0", @@ -7556,9 +7577,9 @@ } }, "node_modules/sharp/node_modules/semver": { - "version": "7.8.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.4.tgz", - "integrity": "sha512-rUCObTnP32Q08R2uuIrt7r9PlEonuTmtuXYcW6s5kjdlj3xbnwe+21yXptAUYcMAABLkYYTtnmzb3w3EDZfueA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "optional": true, "bin": { @@ -8281,16 +8302,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.61.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.0.tgz", - "integrity": "sha512-8y31Rd0eGTrDKqhy6vT0HtzhN+YLjQizwX3aA3hPXP/ynSfnrBXcQY5IzsP9/DM7+klX4IUncZZjkchP0z+rUw==", + "version": "8.61.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.61.1.tgz", + "integrity": "sha512-V7PayAfJokV3pEHgN7/v03D1SpujhRfQtYLbLIiBfDDncdg4PAiRBfoS4cnCANK4jmAPncczi59QO3afiXUlNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.61.0", - "@typescript-eslint/parser": "8.61.0", - "@typescript-eslint/typescript-estree": "8.61.0", - "@typescript-eslint/utils": "8.61.0" + "@typescript-eslint/eslint-plugin": "8.61.1", + "@typescript-eslint/parser": "8.61.1", + "@typescript-eslint/typescript-estree": "8.61.1", + "@typescript-eslint/utils": "8.61.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..11a8516 Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..c73c160 Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/sw.js b/public/sw.js new file mode 100644 index 0000000..11eb0cd --- /dev/null +++ b/public/sw.js @@ -0,0 +1,176 @@ +/* ============================================================ + * Utility Protocol — Service Worker + * Cache name: utility-v1 + * Strategies: + * - Static assets: cache-first (versioned by build hash) + * - API requests: network-first, fallback to cache + * - HTML/navigation: network-first, fallback to cache + * ============================================================ */ + +const CACHE_NAME = "utility-v1"; +const STATIC_CACHE = `${CACHE_NAME}-static`; +const API_CACHE = `${CACHE_NAME}-api`; +const HTML_CACHE = `${CACHE_NAME}-html`; + +// Assets to pre-cache on install (app shell) +const PRE_CACHE_ASSETS = ["/", "/manifest.json", "/icon-192.png", "/icon-512.png"]; + +/* ---------- Install: pre-cache app-shell assets ---------- */ +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(STATIC_CACHE).then((cache) => { + return cache.addAll(PRE_CACHE_ASSETS).catch((err) => { + console.warn("[SW] Pre-cache partial failure:", err.message); + }); + }) + ); + // Activate immediately — don't wait for old tabs to close + self.skipWaiting(); +}); + +/* ---------- Activate: purge stale caches ---------- */ +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => { + const valid = [STATIC_CACHE, API_CACHE, HTML_CACHE]; + return Promise.all( + keys.filter((k) => !valid.includes(k)).map((k) => caches.delete(k)) + ); + }) + ); + // Claim all clients so the new SW controls pages immediately + self.clients.claim(); +}); + +/* ---------- Helper: is the request for an API endpoint? ---------- */ +function isApiRequest(url) { + try { + const { pathname } = new URL(url); + return ( + pathname.startsWith("/api/") || + pathname.includes("/rpc") || + pathname.includes("/graphql") + ); + } catch { + return false; + } +} + +/* ---------- Helper: is the request for a static asset? ---------- */ +function isStaticAsset(url) { + try { + const { pathname } = new URL(url); + return ( + /\.(js|css|png|jpg|jpeg|gif|svg|ico|woff2?|ttf|eot|webp|avif)$/i.test( + pathname + ) || + pathname.startsWith("/_next/static/") || + pathname.startsWith("/_next/image/") || + pathname.startsWith("/icon-") || + pathname === "/manifest.json" + ); + } catch { + return false; + } +} + +/* ---------- Helper: is the request a navigation (HTML) request? ---------- */ +function isNavigationRequest(request) { + return request.mode === "navigate"; +} + +/* ---------- Strategy: Network-first (for API calls) ---------- */ +async function networkFirst(request) { + const cache = await caches.open(API_CACHE); + try { + const response = await fetch(request); + // Only cache successful responses + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + } catch (err) { + const cached = await cache.match(request); + if (cached) { + return cached; + } + throw err; + } +} + +/* ---------- Strategy: Cache-first (for static assets) ---------- */ +async function cacheFirst(request) { + const cache = await caches.open(STATIC_CACHE); + const cached = await cache.match(request); + if (cached) { + // Update cache in background (stale-while-revalidate) + fetch(request) + .then((response) => { + if (response.ok) { + cache.put(request, response); + } + }) + .catch(() => { + /* ignore background update failures */ + }); + return cached; + } + try { + const response = await fetch(request); + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + } catch (err) { + throw err; + } +} + +/* ---------- Strategy: Network-first with HTML cache fallback ---------- */ +async function htmlNetworkFirst(request) { + const cache = await caches.open(HTML_CACHE); + try { + const response = await fetch(request); + if (response.ok) { + cache.put(request, response.clone()); + } + return response; + } catch (err) { + const cached = await cache.match(request); + if (cached) { + return cached; + } + // Final fallback: return the cached root page + const fallback = await cache.match("/"); + if (fallback) { + return fallback; + } + throw err; + } +} + +/* ---------- Fetch event: route to appropriate strategy ---------- */ +self.addEventListener("fetch", (event) => { + const { request } = event; + + // Skip non-GET requests + if (request.method !== "GET") { + return; + } + + // Skip chrome-extension and other non-http(s) URLs + const url = request.url; + if (!url.startsWith("http")) { + return; + } + + // Route based on request type + if (isNavigationRequest(request)) { + event.respondWith(htmlNetworkFirst(request)); + } else if (isApiRequest(url)) { + event.respondWith(networkFirst(request)); + } else if (isStaticAsset(url)) { + event.respondWith(cacheFirst(request)); + } + // Everything else: let the browser handle it normally +}); diff --git a/scripts/generate-icons.js b/scripts/generate-icons.js new file mode 100644 index 0000000..6ba742f --- /dev/null +++ b/scripts/generate-icons.js @@ -0,0 +1,224 @@ +/* eslint-disable */ +/** + * Generates placeholder PWA icons for Utility Protocol. + * Creates simple but recognizable utility/energy-themed icons + * using only Node.js built-ins (zlib for PNG compression). + * + * Design: Dark background (#0a0a0a) with a stylized lightning-bolt + * motif in a teal accent color. + */ + +const zlib = require("zlib"); +const fs = require("fs"); +const path = require("path"); + +const BG = [0x0a, 0x0a, 0x0a, 0xff]; // #0a0a0a +const ACCENT = [0x00, 0xd4, 0xaa, 0xff]; // teal/green accent + +function createCRC(buf) { + // CRC-32 for PNG + let crc = 0xffffffff; + const table = new Int32Array(256); + for (let i = 0; i < 256; i++) { + let c = i; + for (let j = 0; j < 8; j++) { + c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1; + } + table[i] = c; + } + for (let i = 0; i < buf.length; i++) { + crc = table[(crc ^ buf[i]) & 0xff] ^ (crc >>> 8); + } + return (crc ^ 0xffffffff) >>> 0; +} + +function createChunk(type, data) { + const typeBytes = Buffer.from(type, "ascii"); + const lenBuf = Buffer.alloc(4); + lenBuf.writeUInt32BE(data.length, 0); + const crcInput = Buffer.concat([typeBytes, data]); + const crcBuf = Buffer.alloc(4); + crcBuf.writeUInt32BE(createCRC(crcInput), 0); + return Buffer.concat([lenBuf, typeBytes, data, crcBuf]); +} + +function createPNG(width, height, pixels) { + const signature = Buffer.from([ + 137, 80, 78, 71, 13, 10, 26, 10, + ]); + + // IHDR + const ihdrData = Buffer.alloc(13); + ihdrData.writeUInt32BE(width, 0); + ihdrData.writeUInt32BE(height, 4); + ihdrData[8] = 8; // bit depth + ihdrData[9] = 6; // color type: RGBA + ihdrData[10] = 0; // compression + ihdrData[11] = 0; // filter + ihdrData[12] = 0; // interlace + const ihdr = createChunk("IHDR", ihdrData); + + // Raw image data with filter byte per row + const rawData = Buffer.alloc(height * (1 + width * 4)); + for (let y = 0; y < height; y++) { + const rowOffset = y * (1 + width * 4); + rawData[rowOffset] = 0; // filter: none + for (let x = 0; x < width; x++) { + const idx = (y * width + x) * 4; + const dst = rowOffset + 1 + x * 4; + rawData[dst] = pixels[idx]; + rawData[dst + 1] = pixels[idx + 1]; + rawData[dst + 2] = pixels[idx + 2]; + rawData[dst + 3] = pixels[idx + 3]; + } + } + + const compressed = zlib.deflateSync(rawData); + const idat = createChunk("IDAT", compressed); + const iend = createChunk("IEND", Buffer.alloc(0)); + + return Buffer.concat([signature, ihdr, idat, iend]); +} + +/** + * Draw a simple utility/energy icon: + * - Dark rounded square background + * - Stylized lightning-bolt shape in teal + */ +function drawIcon(size) { + const pixels = Buffer.alloc(size * size * 4); + const cx = size / 2; + const cy = size / 2; + const outerR = size * 0.44; + + // Corner radius for rounded rect + const cornerR = size * 0.12; + + function distToRectEdge(px, py) { + const half = outerR; + const left = cx - half; + const right = cx + half; + const top = cy - half; + const bottom = cy + half; + + // Inside rect + if (px >= left + cornerR && px <= right - cornerR && py >= top && py <= bottom) return -1; + if (py >= top + cornerR && py <= bottom - cornerR && px >= left && px <= right) return -1; + + // Distance from corners + function dist(x1, y1, x2, y2) { + return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2); + } + + const corners = [ + [left + cornerR, top + cornerR], + [right - cornerR, top + cornerR], + [left + cornerR, bottom - cornerR], + [right - cornerR, bottom - cornerR], + ]; + + let minDist = Infinity; + for (const [cx2, cy2] of corners) { + minDist = Math.min(minDist, dist(px, py, cx2, cy2) - cornerR); + } + + // Edges + if (px >= left && px <= right) { + minDist = Math.min(minDist, py - top, bottom - py); + } + if (py >= top && py <= bottom) { + minDist = Math.min(minDist, px - left, right - px); + } + + return minDist; + } + + // Draw lightning bolt shape inside the rect + function insideLightning(px, py) { + // Normalize to center + const dx = px - cx; + const dy = py - cy; + const boltWidth = size * 0.2; + + // Lightning bolt: zigzag vertical path + const topY = -size * 0.25; + const midY = 0; + const bottomY = size * 0.25; + const leftX = -boltWidth * 0.3; + const rightX = boltWidth * 0.3; + + function segmentDist(sx, sy, x1, y1, x2, y2) { + const sdx = x2 - x1; + const sdy = y2 - y1; + const lenSq = sdx * sdx + sdy * sdy; + if (lenSq === 0) return Math.sqrt((sx - x1) ** 2 + (sy - y1) ** 2); + + let t = ((sx - x1) * sdx + (sy - y1) * sdy) / lenSq; + t = Math.max(0, Math.min(1, t)); + const projX = x1 + t * sdx; + const projY = y1 + t * sdy; + return Math.sqrt((sx - projX) ** 2 + (sy - projY) ** 2); + } + + // Top segment from (leftX, topY) to (rightX, midY) + const seg1 = segmentDist(dx, dy, leftX, topY, rightX, midY); + // Middle segment + const seg2 = segmentDist(dx, dy, rightX, midY, rightX * 0.5, midY * 0.1); + // Bottom segment + const seg3 = segmentDist(dx, dy, rightX * 0.5, midY * 0.1, leftX, bottomY); + // Bottom-right segment + const seg4 = segmentDist(dx, dy, leftX, bottomY, rightX * 0.4, bottomY * 0.3); + + const minSeg = Math.min(seg1, seg2, seg3, seg4); + return minSeg < boltWidth * 0.25; + } + + for (let y = 0; y < size; y++) { + for (let x = 0; x < size; x++) { + const idx = (y * size + x) * 4; + const d = distToRectEdge(x, y); + + if (d < 0) { + // Inside the rounded rect background + pixels[idx] = BG[0]; + pixels[idx + 1] = BG[1]; + pixels[idx + 2] = BG[2]; + pixels[idx + 3] = BG[3]; + + // Draw the accent shape + if (insideLightning(x, y)) { + pixels[idx] = ACCENT[0]; + pixels[idx + 1] = ACCENT[1]; + pixels[idx + 2] = ACCENT[2]; + pixels[idx + 3] = ACCENT[3]; + } + } else if (d < 1.5) { + // Anti-aliased edge + const alpha = Math.max(0, 1 - d / 1.5); + pixels[idx] = BG[0]; + pixels[idx + 1] = BG[1]; + pixels[idx + 2] = BG[2]; + pixels[idx + 3] = Math.round(BG[3] * alpha); + } else { + // Transparent outside + pixels[idx] = 0; + pixels[idx + 1] = 0; + pixels[idx + 2] = 0; + pixels[idx + 3] = 0; + } + } + } + + return pixels; +} + +// Generate both sizes +const publicDir = path.join(__dirname, "..", "public"); + +[192, 512].forEach((size) => { + const pixels = drawIcon(size); + const png = createPNG(size, size, pixels); + const filePath = path.join(publicDir, `icon-${size}.png`); + fs.writeFileSync(filePath, png); + console.warn(`Created ${filePath} (${png.length} bytes)`); +}); diff --git a/src/app/layout.tsx b/src/app/layout.tsx index b6d3d4f..3ef8779 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -3,6 +3,7 @@ import { Geist, Geist_Mono } from "next/font/google"; import "./globals.css"; import { ThemeProvider } from "@/components/providers/ThemeProvider"; import { WalletProvider } from "@/components/providers/WalletProvider"; +import { ServiceWorkerProvider } from "@/components/providers/ServiceWorkerProvider"; const geistSans = Geist({ variable: "--font-geist-sans", @@ -42,9 +43,11 @@ export default function RootLayout({ - - {children} - + + + {children} + + ); diff --git a/src/components/providers/ServiceWorkerProvider.tsx b/src/components/providers/ServiceWorkerProvider.tsx new file mode 100644 index 0000000..3d1be06 --- /dev/null +++ b/src/components/providers/ServiceWorkerProvider.tsx @@ -0,0 +1,43 @@ +"use client"; + +import { useEffect, useRef } from "react"; + +export function ServiceWorkerProvider({ + children, +}: { + children: React.ReactNode; +}) { + const registered = useRef(false); + + useEffect(() => { + if (registered.current) return; + + if (typeof window === "undefined" || !navigator.serviceWorker) { + return; + } + + registered.current = true; + + navigator.serviceWorker + .register("/sw.js", { scope: "/" }) + .then((registration) => { + // Listen for updates + registration.addEventListener("updatefound", () => { + const installing = registration.installing; + if (!installing) return; + + installing.addEventListener("statechange", () => { + if (installing.state === "installed" && navigator.serviceWorker.controller) { + // New content is available — this will be used on next navigation + console.warn("[SW] New version available"); + } + }); + }); + }) + .catch((err) => { + console.warn("[SW] Registration failed:", err.message); + }); + }, []); + + return <>{children}; +} diff --git a/tests/components/ServiceWorkerProvider.test.tsx b/tests/components/ServiceWorkerProvider.test.tsx new file mode 100644 index 0000000..eeb8876 --- /dev/null +++ b/tests/components/ServiceWorkerProvider.test.tsx @@ -0,0 +1,198 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; +import { render } from "@testing-library/react"; +import { ServiceWorkerProvider } from "../../src/components/providers/ServiceWorkerProvider"; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +function createMockRegistration(): any { + const listeners: Array<{ + type: string; + handler: EventListenerOrEventListenerObject; + }> = []; + const state = "installing"; + + const mockInstalling = { + state, + addEventListener: vi.fn( + (event: string, handler: EventListenerOrEventListenerObject) => { + listeners.push({ type: event, handler }); + } + ), + }; + + const mockRegistration = { + installing: mockInstalling, + addEventListener: vi.fn( + (event: string, handler: EventListenerOrEventListenerObject) => { + listeners.push({ type: event, handler }); + } + ), + _triggerStateChange: (newState: string) => { + mockInstalling.state = newState; + listeners + .filter((l) => l.type === "statechange") + .forEach((l) => { + if (typeof l.handler === "function") { + l.handler(new Event("statechange")); + } else { + l.handler.handleEvent(new Event("statechange")); + } + }); + }, + _triggerUpdateFound: () => { + listeners + .filter((l) => l.type === "updatefound") + .forEach((l) => { + if (typeof l.handler === "function") { + l.handler(new Event("updatefound")); + } else { + l.handler.handleEvent(new Event("updatefound")); + } + }); + }, + }; + + return mockRegistration; +} + +describe("ServiceWorkerProvider", () => { + let originalServiceWorker: ServiceWorkerContainer | undefined; + + beforeEach(() => { + originalServiceWorker = (navigator as { serviceWorker?: ServiceWorkerContainer }) + .serviceWorker; + // @ts-expect-error – mock serviceWorker for tests + navigator.serviceWorker = undefined; + }); + + afterEach(() => { + // @ts-expect-error – restore original + navigator.serviceWorker = originalServiceWorker; + vi.restoreAllMocks(); + }); + + it("should register the service worker when supported", async () => { + const mockReg = createMockRegistration(); + const registerMock = vi.fn().mockResolvedValue(mockReg); + + // @ts-expect-error – mock serviceWorker + navigator.serviceWorker = { + register: registerMock, + controller: null, + addEventListener: vi.fn(), + }; + + render( + +
App content
+
+ ); + + // Wait for useEffect + await vi.waitFor(() => { + expect(registerMock).toHaveBeenCalledWith("/sw.js", { scope: "/" }); + }); + }); + + it("should render children", () => { + // @ts-expect-error – mock serviceWorker + navigator.serviceWorker = { + register: vi.fn().mockResolvedValue(createMockRegistration()), + controller: null, + addEventListener: vi.fn(), + }; + + const { getByText } = render( + +
App content
+
+ ); + + expect(getByText("App content")).toBeDefined(); + }); + + it("should not crash when service workers are unsupported", () => { + // navigator.serviceWorker is already undefined from beforeEach + + const { getByText } = render( + +
No SW support
+
+ ); + + expect(getByText("No SW support")).toBeDefined(); + }); + + it("should handle registration failure gracefully", async () => { + const consoleWarnSpy = vi + .spyOn(console, "warn") + .mockImplementation(() => {}); + + // @ts-expect-error – mock serviceWorker + navigator.serviceWorker = { + register: vi.fn().mockRejectedValue(new Error("SW registration failed")), + controller: null, + addEventListener: vi.fn(), + }; + + const { getByText } = render( + +
Content
+
+ ); + + await vi.waitFor(() => { + expect(consoleWarnSpy).toHaveBeenCalledWith( + "[SW] Registration failed:", + "SW registration failed" + ); + }); + + expect(getByText("Content")).toBeDefined(); + consoleWarnSpy.mockRestore(); + }); + + it("should listen for updatefound event", async () => { + const mockReg = createMockRegistration(); + const registerMock = vi.fn().mockResolvedValue(mockReg); + + // @ts-expect-error – mock serviceWorker + navigator.serviceWorker = { + register: registerMock, + controller: null, + addEventListener: vi.fn(), + }; + + render( + +
Content
+
+ ); + + await vi.waitFor(() => { + expect(registerMock).toHaveBeenCalled(); + }); + + expect(mockReg.addEventListener).toHaveBeenCalledWith( + "updatefound", + expect.any(Function) + ); + }); + + it("should guard against missing navigator.serviceWorker", () => { + // navigator.serviceWorker is already undefined from beforeEach + const registerSpy = vi.fn(); + + // @ts-expect-error – mock serviceWorker + navigator.serviceWorker = undefined; + + // Should not throw + const { getByText } = render( + +
Safe render
+
+ ); + + expect(getByText("Safe render")).toBeDefined(); + expect(registerSpy).not.toHaveBeenCalled(); + }); +});