diff --git a/babel.config.js b/babel.config.ts similarity index 74% rename from babel.config.js rename to babel.config.ts index 2f1045a4..2fea1655 100644 --- a/babel.config.js +++ b/babel.config.ts @@ -1,2 +1,2 @@ -//babel.config.js +//babel.config.ts module.exports = { presets: ["@babel/preset-env"] }; diff --git a/eslint.config.mjs b/eslint.config.mjs index 069624e9..3c87f11e 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -25,7 +25,10 @@ export default defineConfig([ }, { rules: { - "@typescript-eslint/no-unused-vars": ["error", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "prefer-const": "off", + "no-var": "warn", + "@typescript-eslint/no-unused-vars": ["warn", { argsIgnorePattern: "^_", varsIgnorePattern: "^_" }], + "@typescript-eslint/no-explicit-any": "off", }, }, pluginReact.configs.flat.recommended, diff --git a/global.d.ts b/global.d.ts new file mode 100644 index 00000000..9aa2ecd9 --- /dev/null +++ b/global.d.ts @@ -0,0 +1,7 @@ +// Extend the Window interface to include the kopiaUI property +declare interface Window { + kopiaUI?: { + selectDirectory?: (callback: (path: string) => void) => void; + browseDirectory?; + }; +} diff --git a/package-lock.json b/package-lock.json index 59974708..5f727dc8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,6 +27,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^5.1.0", @@ -37,6 +38,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", + "jiti": "^2.6.1", "jsdom": "^27.0.1", "prettier": "^3.7.4", "typescript": "^5.9.3", @@ -1304,6 +1306,85 @@ "node": ">=8" } }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/@jridgewell/gen-mapping": { "version": "0.3.12", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", @@ -1815,6 +1896,13 @@ "win32" ] }, + "node_modules/@sinclair/typebox": { + "version": "0.34.41", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.41.tgz", + "integrity": "sha512-6gS8pZzSXdyRHTIqoqSVknxolr1kzfy4/CeDnrzsVz8TTIWUbOBr6gnzOmTYJ3eXQNh4IYHIGi5aIL7sOZ2G/g==", + "dev": true, + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.17", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.17.tgz", @@ -2042,6 +2130,79 @@ "@types/node": "*" } }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/jest/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@types/jest/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@types/jest/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -2065,22 +2226,22 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.4.tgz", - "integrity": "sha512-EB1yiiYdvySuIITtD5lhW4yPyJ31RkJkkDw794LaQYrxCSaQV/47y5o1FMC4zF9ZyjUjzJMZwbovEnT5yHTW6g==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.0.tgz", + "integrity": "sha512-1LOH8xovvsKsCBq1wnT4ntDUdCJKmnEakhsuoUSy6ExlHCkGP2hqnatagYTgFk6oeL0VU31u7SNjunPN+GchtA==", "license": "MIT", "dependencies": { "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "19.1.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.5.tgz", - "integrity": "sha512-CMCjrWucUBZvohgZxkjd6S9h0nZxXjzus6yDfUb+xLxYM7VvjKNH1tQrE9GWLql1XoOP4/Ds3bwFqShHUYraGg==", + "version": "19.2.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.0.tgz", + "integrity": "sha512-brtBs0MnE9SMx7px208g39lRmC5uHZs96caOJfTjFcYSLHNamvaSMfJNagChVNkup2SdtOxKX1FDBkRSJe1ZAg==", "dev": true, "license": "MIT", "peerDependencies": { - "@types/react": "^19.0.0" + "@types/react": "^19.2.0" } }, "node_modules/@types/react-transition-group": { @@ -2092,12 +2253,36 @@ "@types/react": "*" } }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/warning": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@types/warning/-/warning-3.0.3.tgz", "integrity": "sha512-D1XC7WK8K+zZEveUPY+cf4+kgauk8N4eHr/XIHXGlGYkHLud6hK9lYfZk1ry1TNh798cZUCgb6MqGEG8DkJt6Q==", "license": "MIT" }, + "node_modules/@types/yargs": { + "version": "17.0.33", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", + "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@typescript-eslint/eslint-plugin": { "version": "8.51.0", "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.51.0.tgz", @@ -3032,6 +3217,22 @@ "node": ">= 16" } }, + "node_modules/ci-info": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.1.tgz", + "integrity": "sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/classnames": { "version": "2.5.1", "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", @@ -3851,6 +4052,24 @@ "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", "license": "MIT" }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, "node_modules/expect-type": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.2.1.tgz", @@ -5025,6 +5244,230 @@ "@pkgjs/parseargs": "^0.11.0" } }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-diff/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6418,6 +6861,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -6428,6 +6881,29 @@ "node": ">=0.10.0" } }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", diff --git a/package.json b/package.json index 93392c2a..5d5368f7 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^16.3.0", "@testing-library/user-event": "^14.6.1", + "@types/jest": "^30.0.0", "@types/react": "^19.1.4", "@types/react-dom": "^19.1.5", "@vitejs/plugin-react": "^5.1.0", @@ -55,6 +56,7 @@ "eslint-config-react": "^1.1.7", "eslint-plugin-react": "^7.37.5", "globals": "^16.5.0", + "jiti": "^2.6.1", "jsdom": "^27.0.1", "prettier": "^3.7.4", "typescript": "^5.9.3", diff --git a/src/App.jsx b/src/App.tsx similarity index 92% rename from src/App.jsx rename to src/App.tsx index 0e6a6153..c0931da9 100644 --- a/src/App.jsx +++ b/src/App.tsx @@ -2,7 +2,7 @@ import "bootstrap/dist/css/bootstrap.min.css"; import "./css/Theme.css"; import "./css/App.css"; import axios from "axios"; -import { React, Component } from "react"; +import React, { Component } from "react"; import { Navbar, Nav, Container } from "react-bootstrap"; import { BrowserRouter as Router, NavLink, Navigate, Route, Routes } from "react-router-dom"; import { Policy } from "./pages/Policy"; @@ -19,7 +19,17 @@ import { SnapshotRestore } from "./pages/SnapshotRestore"; import { AppContext } from "./contexts/AppContext"; import { UIPreferenceProvider } from "./contexts/UIPreferencesContext"; -export default class App extends Component { +type AppState = { + runningTaskCount: number; + isFetching: boolean; + repoDescription: string; + isRepositoryConnected: boolean; + uiPrefs?: any; +}; + +export default class App extends Component { + taskSummaryInterval?: number; + constructor() { super(); @@ -35,7 +45,7 @@ export default class App extends Component { this.repositoryDescriptionUpdated = this.repositoryDescriptionUpdated.bind(this); this.fetchInitialRepositoryDescription = this.fetchInitialRepositoryDescription.bind(this); - const tok = document.head.querySelector('meta[name="kopia-csrf-token"]'); + const tok = document.head.querySelector('meta[name="kopia-csrf-token"]') as HTMLMetaElement | null; if (tok && tok.content) { axios.defaults.headers.common["X-Kopia-Csrf-Token"] = tok.content; } else { @@ -54,7 +64,7 @@ export default class App extends Component { this.taskSummaryInterval = window.setInterval(this.fetchTaskSummary, 5000); } - fetchInitialRepositoryDescription() { + fetchInitialRepositoryDescription(): void { axios .get("/api/v1/repo/status") .then((result) => { @@ -70,7 +80,7 @@ export default class App extends Component { }); } - fetchTaskSummary() { + fetchTaskSummary(): void { if (!this.state.isFetching) { this.setState({ isFetching: true }); axios @@ -87,12 +97,12 @@ export default class App extends Component { } } - componentWillUnmount() { + componentWillUnmount(): void { window.clearInterval(this.taskSummaryInterval); } // this is invoked via AppContext whenever repository is connected, disconnected, etc. - repositoryUpdated(isConnected) { + repositoryUpdated(isConnected: boolean): void { this.setState({ isRepositoryConnected: isConnected }); if (isConnected) { window.location.replace("/snapshots"); @@ -101,7 +111,7 @@ export default class App extends Component { } } - repositoryDescriptionUpdated(desc) { + repositoryDescriptionUpdated(desc: string): void { this.setState({ repoDescription: desc, }); @@ -112,7 +122,7 @@ export default class App extends Component { return ( - + diff --git a/src/components/CLIEquivalent.jsx b/src/components/CLIEquivalent.tsx similarity index 100% rename from src/components/CLIEquivalent.jsx rename to src/components/CLIEquivalent.tsx diff --git a/src/components/DirectoryBreadcrumbs.jsx b/src/components/DirectoryBreadcrumbs.tsx similarity index 100% rename from src/components/DirectoryBreadcrumbs.jsx rename to src/components/DirectoryBreadcrumbs.tsx diff --git a/src/components/DirectoryItems.jsx b/src/components/DirectoryItems.tsx similarity index 100% rename from src/components/DirectoryItems.jsx rename to src/components/DirectoryItems.tsx diff --git a/src/components/GoBackButton.jsx b/src/components/GoBackButton.tsx similarity index 100% rename from src/components/GoBackButton.jsx rename to src/components/GoBackButton.tsx diff --git a/src/components/KopiaTable.jsx b/src/components/KopiaTable.tsx similarity index 100% rename from src/components/KopiaTable.jsx rename to src/components/KopiaTable.tsx diff --git a/src/components/Logs.jsx b/src/components/Logs.tsx similarity index 85% rename from src/components/Logs.jsx rename to src/components/Logs.tsx index bfbc37d5..8299c36e 100644 --- a/src/components/Logs.jsx +++ b/src/components/Logs.tsx @@ -4,10 +4,19 @@ import Table from "react-bootstrap/Table"; import { handleChange } from "../forms"; import { redirect } from "../utils/uiutil"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class Logs extends Component { - constructor() { - super(); +interface LogsProps { + taskID: string; +} + +export class Logs extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + interval: number; + messagesEndRef: React.RefObject; + + constructor(props: LogsProps) { + super(props); this.state = { items: [], isLoading: false, @@ -46,7 +55,7 @@ export class Logs extends Component { axios .get("/api/v1/tasks/" + this.props.taskID + "/logs") .then((result) => { - let oldLogs = this.state.logs; + const oldLogs = this.state.logs; this.setState({ logs: result.data.logs, isLoading: false, @@ -65,11 +74,11 @@ export class Logs extends Component { }); } - fullLogTime(x) { + fullLogTime(x: number) { return new Date(x * 1000).toLocaleString(); } - formatLogTime(x) { + formatLogTime(x: number) { const d = new Date(x * 1000); let result = ""; @@ -88,7 +97,7 @@ export class Logs extends Component { // if there are any properties other than `msg, ts, level, mod` output them as JSON. // eslint-disable-next-line @typescript-eslint/no-unused-vars - let { msg, ts, level, mod, ...parametersOnly } = entry; + const { msg, ts, level, mod, ...parametersOnly } = entry; const p = JSON.stringify(parametersOnly); if (p !== "{}") { diff --git a/src/components/PolicyEditorLink.jsx b/src/components/PolicyEditorLink.tsx similarity index 51% rename from src/components/PolicyEditorLink.jsx rename to src/components/PolicyEditorLink.tsx index 883c006e..6d5bd129 100644 --- a/src/components/PolicyEditorLink.jsx +++ b/src/components/PolicyEditorLink.tsx @@ -1,7 +1,7 @@ import React from "react"; import { Link } from "react-router-dom"; -import { PolicyTypeName, policyEditorURL } from "../utils/policyutil"; +import { PolicyKey, PolicyTypeName, policyEditorURL } from "../utils/policyutil"; -export function PolicyEditorLink(s) { +export function PolicyEditorLink(s: PolicyKey) { return {PolicyTypeName(s)}; } diff --git a/src/components/SetupRepository.jsx b/src/components/SetupRepository.tsx similarity index 99% rename from src/components/SetupRepository.jsx rename to src/components/SetupRepository.tsx index d5438a4a..245af54d 100644 --- a/src/components/SetupRepository.jsx +++ b/src/components/SetupRepository.tsx @@ -23,6 +23,7 @@ import { SetupRepositorySFTP } from "./SetupRepositorySFTP"; import { SetupRepositoryToken } from "./SetupRepositoryToken"; import { SetupRepositoryWebDAV } from "./SetupRepositoryWebDAV"; import { toAlgorithmOption } from "../utils/uiutil"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; const supportedProviders = [ { @@ -73,7 +74,9 @@ const supportedProviders = [ }, ]; -export class SetupRepository extends Component { +export class SetupRepository extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor() { super(); diff --git a/src/components/SetupRepositoryAzure.jsx b/src/components/SetupRepositoryAzure.tsx similarity index 89% rename from src/components/SetupRepositoryAzure.jsx rename to src/components/SetupRepositoryAzure.tsx index 283c9d92..9f893b94 100644 --- a/src/components/SetupRepositoryAzure.jsx +++ b/src/components/SetupRepositoryAzure.tsx @@ -4,7 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; -export class SetupRepositoryAzure extends Component { +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryAzure extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryB2.jsx b/src/components/SetupRepositoryB2.tsx similarity index 87% rename from src/components/SetupRepositoryB2.jsx rename to src/components/SetupRepositoryB2.tsx index 95edf041..3696eaa3 100644 --- a/src/components/SetupRepositoryB2.jsx +++ b/src/components/SetupRepositoryB2.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { RequiredField } from "../forms/RequiredField"; import { OptionalField } from "../forms/OptionalField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryB2 extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryB2 extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryFilesystem.jsx b/src/components/SetupRepositoryFilesystem.tsx similarity index 75% rename from src/components/SetupRepositoryFilesystem.jsx rename to src/components/SetupRepositoryFilesystem.tsx index 9ad2500e..d3aab12b 100644 --- a/src/components/SetupRepositoryFilesystem.jsx +++ b/src/components/SetupRepositoryFilesystem.tsx @@ -2,9 +2,12 @@ import React, { Component } from "react"; import { handleChange, validateRequiredFields } from "../forms"; import { RequiredDirectory } from "../forms/RequiredDirectory"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class SetupRepositoryFilesystem extends Component { - constructor(props) { +export class SetupRepositoryFilesystem extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + + constructor(props: any) { super(); this.state = { diff --git a/src/components/SetupRepositoryGCS.jsx b/src/components/SetupRepositoryGCS.tsx similarity index 87% rename from src/components/SetupRepositoryGCS.jsx rename to src/components/SetupRepositoryGCS.tsx index 426bb104..e75b4a06 100644 --- a/src/components/SetupRepositoryGCS.jsx +++ b/src/components/SetupRepositoryGCS.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryGCS extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryGCS extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryRclone.jsx b/src/components/SetupRepositoryRclone.tsx similarity index 83% rename from src/components/SetupRepositoryRclone.jsx rename to src/components/SetupRepositoryRclone.tsx index 50adb98c..b6a6fead 100644 --- a/src/components/SetupRepositoryRclone.jsx +++ b/src/components/SetupRepositoryRclone.tsx @@ -4,8 +4,11 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryRclone extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryRclone extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryS3.jsx b/src/components/SetupRepositoryS3.tsx similarity index 91% rename from src/components/SetupRepositoryS3.jsx rename to src/components/SetupRepositoryS3.tsx index b7da3877..74f3c1be 100644 --- a/src/components/SetupRepositoryS3.jsx +++ b/src/components/SetupRepositoryS3.tsx @@ -5,8 +5,11 @@ import { OptionalField } from "../forms/OptionalField"; import { RequiredBoolean } from "../forms/RequiredBoolean"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryS3 extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryS3 extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositorySFTP.jsx b/src/components/SetupRepositorySFTP.tsx similarity index 96% rename from src/components/SetupRepositorySFTP.jsx rename to src/components/SetupRepositorySFTP.tsx index 8bd2bc21..6a96615c 100644 --- a/src/components/SetupRepositorySFTP.jsx +++ b/src/components/SetupRepositorySFTP.tsx @@ -6,6 +6,7 @@ import { OptionalNumberField } from "../forms/OptionalNumberField"; import { RequiredBoolean } from "../forms/RequiredBoolean"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; function hasExactlyOneOf(component, names) { let count = 0; @@ -19,7 +20,9 @@ function hasExactlyOneOf(component, names) { return count === 1; } -export class SetupRepositorySFTP extends Component { +export class SetupRepositorySFTP extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryServer.jsx b/src/components/SetupRepositoryServer.tsx similarity index 84% rename from src/components/SetupRepositoryServer.jsx rename to src/components/SetupRepositoryServer.tsx index 2a930e3d..9fbfd2f3 100644 --- a/src/components/SetupRepositoryServer.jsx +++ b/src/components/SetupRepositoryServer.tsx @@ -4,7 +4,10 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; -export class SetupRepositoryServer extends Component { +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; +export class SetupRepositoryServer extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor(props) { super(); diff --git a/src/components/SetupRepositoryToken.jsx b/src/components/SetupRepositoryToken.tsx similarity index 80% rename from src/components/SetupRepositoryToken.jsx rename to src/components/SetupRepositoryToken.tsx index 0da23324..df6a8e27 100644 --- a/src/components/SetupRepositoryToken.jsx +++ b/src/components/SetupRepositoryToken.tsx @@ -3,8 +3,11 @@ import Row from "react-bootstrap/Row"; import { handleChange, validateRequiredFields } from "../forms"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; + +export class SetupRepositoryToken extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; -export class SetupRepositoryToken extends Component { constructor(props) { super(); diff --git a/src/components/SetupRepositoryWebDAV.jsx b/src/components/SetupRepositoryWebDAV.tsx similarity index 82% rename from src/components/SetupRepositoryWebDAV.jsx rename to src/components/SetupRepositoryWebDAV.tsx index 5847e1e3..4117bfd0 100644 --- a/src/components/SetupRepositoryWebDAV.jsx +++ b/src/components/SetupRepositoryWebDAV.tsx @@ -4,9 +4,12 @@ import { handleChange, validateRequiredFields } from "../forms"; import { OptionalField } from "../forms/OptionalField"; import { RequiredField } from "../forms/RequiredField"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "./types"; -export class SetupRepositoryWebDAV extends Component { - constructor(props) { +export class SetupRepositoryWebDAV extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + + constructor(props: any) { super(); this.state = { diff --git a/src/components/SnapshotEstimation.jsx b/src/components/SnapshotEstimation.tsx similarity index 99% rename from src/components/SnapshotEstimation.jsx rename to src/components/SnapshotEstimation.tsx index d65bca00..993b47ab 100644 --- a/src/components/SnapshotEstimation.jsx +++ b/src/components/SnapshotEstimation.tsx @@ -13,6 +13,8 @@ import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import { useNavigate, useLocation, useParams } from "react-router-dom"; export class SnapshotEstimationInternal extends Component { + interval: number | null; + constructor() { super(); this.state = { diff --git a/src/components/notifications/EmailNotificationMethod.jsx b/src/components/notifications/EmailNotificationMethod.tsx similarity index 100% rename from src/components/notifications/EmailNotificationMethod.jsx rename to src/components/notifications/EmailNotificationMethod.tsx diff --git a/src/components/notifications/NotificationEditor.jsx b/src/components/notifications/NotificationEditor.tsx similarity index 100% rename from src/components/notifications/NotificationEditor.jsx rename to src/components/notifications/NotificationEditor.tsx diff --git a/src/components/notifications/NotificationFormatSelector.jsx b/src/components/notifications/NotificationFormatSelector.tsx similarity index 100% rename from src/components/notifications/NotificationFormatSelector.jsx rename to src/components/notifications/NotificationFormatSelector.tsx diff --git a/src/components/notifications/PushoverNotificationMethod.jsx b/src/components/notifications/PushoverNotificationMethod.tsx similarity index 100% rename from src/components/notifications/PushoverNotificationMethod.jsx rename to src/components/notifications/PushoverNotificationMethod.tsx diff --git a/src/components/notifications/WebHookNotificationMethod.jsx b/src/components/notifications/WebHookNotificationMethod.tsx similarity index 100% rename from src/components/notifications/WebHookNotificationMethod.jsx rename to src/components/notifications/WebHookNotificationMethod.tsx diff --git a/src/components/policy-editor/ActionRowMode.jsx b/src/components/policy-editor/ActionRowMode.tsx similarity index 94% rename from src/components/policy-editor/ActionRowMode.jsx rename to src/components/policy-editor/ActionRowMode.tsx index ded41ec4..0364ec8e 100644 --- a/src/components/policy-editor/ActionRowMode.jsx +++ b/src/components/policy-editor/ActionRowMode.tsx @@ -6,7 +6,7 @@ import { LabelColumn } from "./LabelColumn"; import { WideValueColumn } from "./WideValueColumn"; import { EffectiveValue } from "./EffectiveValue"; -export function ActionRowMode(component, action) { +export function ActionRowMode(component, action: string) { return ( diff --git a/src/components/policy-editor/EffectiveTextAreaValue.jsx b/src/components/policy-editor/EffectiveTextAreaValue.tsx similarity index 97% rename from src/components/policy-editor/EffectiveTextAreaValue.jsx rename to src/components/policy-editor/EffectiveTextAreaValue.tsx index 9ce63edf..9de14d22 100644 --- a/src/components/policy-editor/EffectiveTextAreaValue.jsx +++ b/src/components/policy-editor/EffectiveTextAreaValue.tsx @@ -13,7 +13,7 @@ export function EffectiveTextAreaValue(component, policyField) { data-testid={"effective-" + policyField} size="sm" as="textarea" - rows="5" + rows={5} value={getDeepStateProperty(component, "resolved.effective." + policyField, undefined)} readOnly={true} /> diff --git a/src/components/policy-editor/EffectiveTimesOfDayValue.jsx b/src/components/policy-editor/EffectiveTimesOfDayValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveTimesOfDayValue.jsx rename to src/components/policy-editor/EffectiveTimesOfDayValue.tsx diff --git a/src/components/policy-editor/EffectiveValue.jsx b/src/components/policy-editor/EffectiveValue.tsx similarity index 100% rename from src/components/policy-editor/EffectiveValue.jsx rename to src/components/policy-editor/EffectiveValue.tsx diff --git a/src/components/policy-editor/EffectiveValueColumn.jsx b/src/components/policy-editor/EffectiveValueColumn.tsx similarity index 100% rename from src/components/policy-editor/EffectiveValueColumn.jsx rename to src/components/policy-editor/EffectiveValueColumn.tsx diff --git a/src/components/policy-editor/LabelColumn.jsx b/src/components/policy-editor/LabelColumn.tsx similarity index 100% rename from src/components/policy-editor/LabelColumn.jsx rename to src/components/policy-editor/LabelColumn.tsx diff --git a/src/components/policy-editor/PolicyEditor.jsx b/src/components/policy-editor/PolicyEditor.tsx similarity index 95% rename from src/components/policy-editor/PolicyEditor.jsx rename to src/components/policy-editor/PolicyEditor.tsx index 7d4c5d30..4a7b0368 100644 --- a/src/components/policy-editor/PolicyEditor.jsx +++ b/src/components/policy-editor/PolicyEditor.tsx @@ -26,7 +26,7 @@ import { OptionalNumberField } from "../../forms/OptionalNumberField"; import { RequiredBoolean } from "../../forms/RequiredBoolean"; import { TimesOfDayList } from "../../forms/TimesOfDayList"; import { errorAlert, toAlgorithmOption } from "../../utils/uiutil"; -import { sourceQueryStringParams } from "../../utils/policyutil"; +import { PolicyKey, sourceQueryStringParams } from "../../utils/policyutil"; import { PolicyEditorLink } from "../PolicyEditorLink"; import { LabelColumn } from "./LabelColumn"; import { ValueColumn } from "./ValueColumn"; @@ -42,9 +42,43 @@ import { SectionHeaderRow } from "./SectionHeaderRow"; import { ActionRowScript } from "./ActionRowScript"; import { ActionRowTimeout } from "./ActionRowTimeout"; import { ActionRowMode } from "./ActionRowMode"; -import PropTypes from "prop-types"; +import { ChangeEventHandle, ComponentChangeHandling } from "../types"; + +type PolicyEditorProps = { + path?: string; + close: () => void; + embedded?: boolean; + isNew?: boolean; + params?: object; + navigate?: () => void; + location?: object; + userName?: string; + host?: string; +} & PolicyKey; + +type PolicyEditorState = { + items: any[]; + isLoading: boolean; + error: Error | null; + algorithms?: any; + policy?: any; + resolved?: any; + resolvedError?: Error; + isNew?: boolean; + saving?: boolean; +}; + +type Policy = { + files?: any; + compression?: any; + scheduling?: any; + actions?: any; +}; + +export class PolicyEditor extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + lastResolvedPolicy?: any; -export class PolicyEditor extends Component { constructor() { super(); this.state = { @@ -74,7 +108,7 @@ export class PolicyEditor extends Component { }); } - componentDidUpdate(prevProps) { + componentDidUpdate(prevProps: PolicyEditorProps, _prevState: PolicyEditorState, _snapshot: unknown) { if (sourceQueryStringParams(this.props) !== sourceQueryStringParams(prevProps)) { this.fetchPolicy(this.props); } @@ -86,7 +120,7 @@ export class PolicyEditor extends Component { } } - fetchPolicy(props) { + fetchPolicy(props: PolicyEditorProps) { axios .get(this.policyURL(props)) .then((result) => { @@ -111,7 +145,7 @@ export class PolicyEditor extends Component { }); } - resolvePolicy(props) { + resolvePolicy(props: PolicyKey) { const u = "/api/v1/policy/resolve?" + sourceQueryStringParams(props); try { @@ -131,7 +165,7 @@ export class PolicyEditor extends Component { } } - PolicyDefinitionPoint(p) { + PolicyDefinitionPoint(p: PolicyKey) { if (!p) { return ""; } @@ -149,7 +183,7 @@ export class PolicyEditor extends Component { return l; } - let result = []; + const result = []; for (let i = 0; i < l.length; i++) { const s = l[i]; if (s === "") { @@ -174,7 +208,7 @@ export class PolicyEditor extends Component { } // clone and clean up policy before saving - let policy = JSON.parse(JSON.stringify(this.state.policy)); + const policy: Policy = JSON.parse(JSON.stringify(this.state.policy)); if (policy.files) { if (policy.files.ignore) { policy.files.ignore = removeEmpty(policy.files.ignore); @@ -211,7 +245,7 @@ export class PolicyEditor extends Component { return policy; } - sanitizeActions(actions, actionTypes) { + sanitizeActions(actions, actionTypes: string[]) { actionTypes.forEach((actionType) => { if (actions[actionType]) { if (actions[actionType].script === undefined || actions[actionType].script === "") { @@ -226,7 +260,7 @@ export class PolicyEditor extends Component { return actions; } - saveChanges(e) { + saveChanges(e: React.FormEvent | React.MouseEvent): void { e.preventDefault(); try { @@ -264,8 +298,8 @@ export class PolicyEditor extends Component { } } - policyURL(props) { - return "/api/v1/policy?" + sourceQueryStringParams(props); + policyURL(props: PolicyKey) { + return `/api/v1/policy?${sourceQueryStringParams(props)}`; } isGlobal() { @@ -852,15 +886,3 @@ export class PolicyEditor extends Component { ); } } - -PolicyEditor.propTypes = { - path: PropTypes.string, - close: PropTypes.func, - embedded: PropTypes.bool, - isNew: PropTypes.bool, - params: PropTypes.object.isRequired, - navigate: PropTypes.func.isRequired, - location: PropTypes.object.isRequired, - userName: PropTypes.string, - host: PropTypes.string, -}; diff --git a/src/components/policy-editor/SectionHeaderRow.jsx b/src/components/policy-editor/SectionHeaderRow.tsx similarity index 100% rename from src/components/policy-editor/SectionHeaderRow.jsx rename to src/components/policy-editor/SectionHeaderRow.tsx diff --git a/src/components/policy-editor/UpcomingSnapshotTimes.jsx b/src/components/policy-editor/UpcomingSnapshotTimes.tsx similarity index 77% rename from src/components/policy-editor/UpcomingSnapshotTimes.jsx rename to src/components/policy-editor/UpcomingSnapshotTimes.tsx index ae8ac902..004303ec 100644 --- a/src/components/policy-editor/UpcomingSnapshotTimes.jsx +++ b/src/components/policy-editor/UpcomingSnapshotTimes.tsx @@ -2,7 +2,7 @@ import moment from "moment"; import React from "react"; import { LabelColumn } from "./LabelColumn"; -export function UpcomingSnapshotTimes(resolved) { +export function UpcomingSnapshotTimes(resolved: { schedulingError?: string; upcomingSnapshotTimes: string[] } | null) { if (!resolved) { return null; } @@ -11,7 +11,7 @@ export function UpcomingSnapshotTimes(resolved) { return

{resolved.schedulingError}

; } - const times = resolved.upcomingSnapshotTimes; + const times: string[] = resolved.upcomingSnapshotTimes; if (!times) { return ; diff --git a/src/components/policy-editor/ValueColumn.jsx b/src/components/policy-editor/ValueColumn.tsx similarity index 100% rename from src/components/policy-editor/ValueColumn.jsx rename to src/components/policy-editor/ValueColumn.tsx diff --git a/src/components/policy-editor/WideValueColumn.jsx b/src/components/policy-editor/WideValueColumn.tsx similarity index 100% rename from src/components/policy-editor/WideValueColumn.jsx rename to src/components/policy-editor/WideValueColumn.tsx diff --git a/src/components/types.ts b/src/components/types.ts new file mode 100644 index 00000000..e8d54294 --- /dev/null +++ b/src/components/types.ts @@ -0,0 +1,7 @@ +import { Component } from "react"; + +export type ChangeEventHandle = (event: React.ChangeEvent, valueGetter?: (x: any) => any) => void; + +export type ComponentChangeHandling = Component & { + handleChange: ChangeEventHandle; +}; diff --git a/src/contexts/UIPreferencesContext.tsx b/src/contexts/UIPreferencesContext.tsx index 985f80c9..cb08df6f 100644 --- a/src/contexts/UIPreferencesContext.tsx +++ b/src/contexts/UIPreferencesContext.tsx @@ -1,7 +1,7 @@ import React, { ReactNode, useCallback, useEffect, useState } from "react"; import axios from "axios"; -export const PAGE_SIZES = [10, 20, 30, 40, 50, 100]; +export const PAGE_SIZES: PageSize[] = [10, 20, 30, 40, 50, 100]; export const UIPreferencesContext = React.createContext({} as UIPreferences); const DEFAULT_PREFERENCES = { diff --git a/src/forms/LogDetailSelector.jsx b/src/forms/LogDetailSelector.tsx similarity index 84% rename from src/forms/LogDetailSelector.jsx rename to src/forms/LogDetailSelector.tsx index 97ebdec2..87fd2eff 100644 --- a/src/forms/LogDetailSelector.jsx +++ b/src/forms/LogDetailSelector.tsx @@ -1,8 +1,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import { valueToNumber, stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function LogDetailSelector(component, name) { +export function LogDetailSelector(component: ComponentChangeHandling, name: string) { return ( {label && {label}} diff --git a/src/forms/OptionalDirectory.jsx b/src/forms/OptionalDirectory.tsx similarity index 86% rename from src/forms/OptionalDirectory.jsx rename to src/forms/OptionalDirectory.tsx index d2e60c61..afc6a064 100644 --- a/src/forms/OptionalDirectory.jsx +++ b/src/forms/OptionalDirectory.tsx @@ -6,6 +6,7 @@ import { faFolderOpen } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { stateProperty } from "."; import { setDeepStateProperty } from "../utils/deepstate"; +import { ComponentChangeHandling } from "src/components/types"; /** * This functions returns a directory selector that allows the user to select a directory. @@ -22,7 +23,7 @@ import { setDeepStateProperty } from "../utils/deepstate"; * Additional properties of the component * @returns The form group with the components */ -export function OptionalDirectory(component, label, name, props = {}) { +export function OptionalDirectory(component: ComponentChangeHandling, label: string | null, name: string, props = {}) { /** * Saves the selected path as a deepstate variable within the component * @param {The path that has been selected} path @@ -49,7 +50,7 @@ export function OptionalDirectory(component, label, name, props = {}) { {...props} > {window.kopiaUI && ( - )} diff --git a/src/forms/OptionalField.jsx b/src/forms/OptionalField.tsx similarity index 72% rename from src/forms/OptionalField.jsx rename to src/forms/OptionalField.tsx index 4b0e7e65..137539e1 100644 --- a/src/forms/OptionalField.jsx +++ b/src/forms/OptionalField.tsx @@ -2,8 +2,15 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function OptionalField(component, label, name, props = {}, helpText = null) { +export function OptionalField( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText = null, +) { return ( {label} diff --git a/src/forms/OptionalFieldNoLabel.jsx b/src/forms/OptionalFieldNoLabel.tsx similarity index 71% rename from src/forms/OptionalFieldNoLabel.jsx rename to src/forms/OptionalFieldNoLabel.tsx index 49d1db2f..33a086d7 100644 --- a/src/forms/OptionalFieldNoLabel.jsx +++ b/src/forms/OptionalFieldNoLabel.tsx @@ -3,8 +3,16 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function OptionalFieldNoLabel(component, label, name, props = {}, helpText = null, invalidFeedback = null) { +export function OptionalFieldNoLabel( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText = null, + invalidFeedback = null, +) { return ( {label && {label}} diff --git a/src/forms/RequiredBoolean.jsx b/src/forms/RequiredBoolean.tsx similarity index 80% rename from src/forms/RequiredBoolean.jsx rename to src/forms/RequiredBoolean.tsx index 841bba85..34ffc9e5 100644 --- a/src/forms/RequiredBoolean.jsx +++ b/src/forms/RequiredBoolean.tsx @@ -3,7 +3,7 @@ import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; -function checkedToBool(t) { +function checkedToBool(t: HTMLInputElement): boolean { if (t.checked) { return true; } @@ -11,7 +11,7 @@ function checkedToBool(t) { return false; } -export function RequiredBoolean(component, label, name, helpText) { +export function RequiredBoolean(component, label: string, name: string, helpText?: string) { return ( {window.kopiaUI && ( - )} diff --git a/src/forms/RequiredField.jsx b/src/forms/RequiredField.tsx similarity index 76% rename from src/forms/RequiredField.jsx rename to src/forms/RequiredField.tsx index 8ed414fe..f6d2959e 100644 --- a/src/forms/RequiredField.jsx +++ b/src/forms/RequiredField.tsx @@ -2,8 +2,15 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function RequiredField(component, label, name, props = {}, helpText = null) { +export function RequiredField( + component: ComponentChangeHandling, + label: string, + name: string, + props = {}, + helpText: string | null = null, +) { return ( {label} diff --git a/src/forms/RequiredNumberField.jsx b/src/forms/RequiredNumberField.tsx similarity index 80% rename from src/forms/RequiredNumberField.jsx rename to src/forms/RequiredNumberField.tsx index df3e3e37..96aa357f 100644 --- a/src/forms/RequiredNumberField.jsx +++ b/src/forms/RequiredNumberField.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty, isInvalidNumber, valueToNumber } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function RequiredNumberField(component, label, name, props = {}) { +export function RequiredNumberField(component: ComponentChangeHandling, label: string, name: string, props = {}) { return ( {label} diff --git a/src/forms/StringList.jsx b/src/forms/StringList.tsx similarity index 64% rename from src/forms/StringList.jsx rename to src/forms/StringList.tsx index 1aaaf6ff..e5db2607 100644 --- a/src/forms/StringList.jsx +++ b/src/forms/StringList.tsx @@ -2,8 +2,9 @@ import React from "react"; import Form from "react-bootstrap/Form"; import Col from "react-bootstrap/Col"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function listToMultilineString(v) { +export function listToMultilineString(v: undefined | null | string[]): string { if (v) { return v.join("\n"); } @@ -11,7 +12,7 @@ export function listToMultilineString(v) { return ""; } -export function multilineStringToList(target) { +export function multilineStringToList(target: HTMLTextAreaElement): string[] | undefined { const v = target.value; if (v === "") { return undefined; @@ -20,7 +21,7 @@ export function multilineStringToList(target) { return v.split(/\n/); } -export function StringList(component, name, props = {}) { +export function StringList(component: ComponentChangeHandling, name: string, props = {}) { return ( component.handleChange(e, multilineStringToList)} as="textarea" - rows="5" + rows={5} {...props} > diff --git a/src/forms/TimesOfDayList.jsx b/src/forms/TimesOfDayList.tsx similarity index 71% rename from src/forms/TimesOfDayList.jsx rename to src/forms/TimesOfDayList.tsx index d9ed820b..658fabce 100644 --- a/src/forms/TimesOfDayList.jsx +++ b/src/forms/TimesOfDayList.tsx @@ -2,10 +2,11 @@ import React from "react"; import Form from "react-bootstrap/Form"; import FormGroup from "react-bootstrap/FormGroup"; import { stateProperty } from "."; +import { ComponentChangeHandling } from "src/components/types"; -export function TimesOfDayList(component, name, props = {}) { - function parseTimeOfDay(v) { - var re = /(\d+):(\d+)/; +export function TimesOfDayList(component: ComponentChangeHandling, name: string, props = {}) { + function parseTimeOfDay(v: string): { hour: number; min: number } | string { + const re = /(\d+):(\d+)/; const match = re.exec(v); if (match) { @@ -25,9 +26,9 @@ export function TimesOfDayList(component, name, props = {}) { return v; } - function toMultilineString(v) { + function toMultilineString(v): string { if (v) { - let tmp = []; + const tmp = []; for (const tod of v) { if (typeof tod === "object") { @@ -43,18 +44,16 @@ export function TimesOfDayList(component, name, props = {}) { return ""; } - function fromMultilineString(target) { + function fromMultilineString(target: HTMLTextAreaElement): ({ hour: number; min: number } | string)[] | undefined { const v = target.value; if (v === "") { return undefined; } - let result = []; - + const result: ({ hour: number; min: number } | string)[] = []; for (const line of v.split(/\n/)) { result.push(parseTimeOfDay(line)); } - return result; } @@ -66,7 +65,7 @@ export function TimesOfDayList(component, name, props = {}) { value={toMultilineString(stateProperty(component, name))} onChange={(e) => component.handleChange(e, fromMultilineString)} as="textarea" - rows="5" + rows={5} {...props} > Invalid Times of Day diff --git a/src/forms/index.jsx b/src/forms/index.tsx similarity index 71% rename from src/forms/index.jsx rename to src/forms/index.tsx index ce1f36fc..96a2c9e8 100644 --- a/src/forms/index.jsx +++ b/src/forms/index.tsx @@ -1,7 +1,9 @@ +import { Component } from "react"; import { getDeepStateProperty, setDeepStateProperty } from "../utils/deepstate"; +import { ComponentChangeHandling } from "src/components/types"; -export function validateRequiredFields(component, fields) { - let updateState = {}; +export function validateRequiredFields(component: ComponentChangeHandling, fields: string[]) { + const updateState: { [field: string]: any } = {}; let failed = false; for (let i = 0; i < fields.length; i++) { @@ -26,7 +28,7 @@ export function handleChange(event, valueGetter = (x) => x.value) { setDeepStateProperty(this, event.target.name, valueGetter(event.target)); } -export function stateProperty(component, name, defaultValue = "") { +export function stateProperty(component: Component, name: string, defaultValue: string | null | undefined = "") { const value = getDeepStateProperty(component, name); return value === undefined ? defaultValue : value; } @@ -44,7 +46,7 @@ export function valueToNumber(t) { return v; } -export function isInvalidNumber(v) { +export function isInvalidNumber(v: any): boolean { if (v === undefined || v === "") { return false; } diff --git a/src/index.jsx b/src/index.jsx deleted file mode 100644 index fccc23cb..00000000 --- a/src/index.jsx +++ /dev/null @@ -1,7 +0,0 @@ -import React from "react"; -import { createRoot } from "react-dom/client"; -import App from "./App"; -import "./css/index.css"; - -const root = createRoot(document.getElementById("root")); -root.render(); diff --git a/src/index.tsx b/src/index.tsx new file mode 100644 index 00000000..195cb560 --- /dev/null +++ b/src/index.tsx @@ -0,0 +1,7 @@ +import React from "react"; +import { Container, createRoot } from "react-dom/client"; +import App from "./App"; +import "./css/index.css"; + +const root = createRoot(document.getElementById("root") as Container); +root.render(); diff --git a/src/pages/Policies.jsx b/src/pages/Policies.tsx similarity index 91% rename from src/pages/Policies.jsx rename to src/pages/Policies.tsx index 179f1507..a36b175c 100644 --- a/src/pages/Policies.jsx +++ b/src/pages/Policies.tsx @@ -17,6 +17,7 @@ import { compare, formatOwnerName } from "../utils/formatutils"; import { redirect } from "../utils/uiutil"; import { checkPolicyPath, policyEditorURL } from "../utils/policyutil"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; const applicablePolicies = "Applicable Policies"; const localPolicies = "Local Path Policies"; @@ -25,7 +26,23 @@ const globalPolicy = "Global Policy"; const perUserPolicies = "Per-User Policies"; const perHostPolicies = "Per-Host Policies"; -export class PoliciesInternal extends Component { +interface PoliciesState { + policies: any[]; + isLoading: boolean; + error: Error | null; + editorTarget: any; + selectedOwner: string; + policyPath: string; + sources: any[]; + localHost?: string; + localUsername?: string; + localSourceName?: string; + multiUser?: any; +} + +export class PoliciesInternal extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + constructor() { super(); this.state = { @@ -116,33 +133,32 @@ export class PoliciesInternal extends Component { return; } - const error = checkPolicyPath(this.state.policyPath, this.state.localHost, this.state.localUsername); + const error = checkPolicyPath(this.state.policyPath); if (error) { alert( - error + - "\nMust be either an absolute path, `user@host:/absolute/path`, `user@host` or `@host`. Use backslashes on Windows.", + `${error}\nMust be either an absolute path, \`user@host:/absolute/path\`, \`user@host\` or \`@host\`. Use backslashes on Windows.`, ); return; } this.props.navigate( policyEditorURL({ - userName: this.state.localUsername, - host: this.state.localHost, + userName: this.state.localUsername!, + host: this.state.localHost!, path: this.state.policyPath, }), ); } - selectOwner(h) { + selectOwner(h: string) { this.setState({ selectedOwner: h, }); } policySummary(policies) { - let bits = []; + const bits = []; /** * Check if the object is empty * @param {*} obj @@ -161,13 +177,13 @@ export class PoliciesInternal extends Component { * @param {*} obj * @returns */ - function isEmpty(obj) { - for (var key in obj) { + function isEmpty(obj: any) { + for (const key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) return isEmptyObject(obj[key]); } return true; } - for (let pol in policies.policy) { + for (const pol in policies.policy) { if (!isEmpty(policies.policy[pol])) { bits.push( @@ -200,7 +216,7 @@ export class PoliciesInternal extends Component { return

Loading ...

; } - let uniqueOwners = sources.reduce((a, d) => { + const uniqueOwners: any[] = sources.reduce((a, d) => { const owner = formatOwnerName(d.source); if (!a.includes(owner)) { @@ -215,29 +231,23 @@ export class PoliciesInternal extends Component { case allPolicies: // do nothing; break; - case globalPolicy: policies = policies.filter((x) => this.isGlobalPolicy(x)); break; - case localPolicies: policies = policies.filter((x) => this.isLocalUserPolicy(x)); break; - case applicablePolicies: policies = policies.filter( (x) => this.isLocalUserPolicy(x) || this.isLocalHostPolicy(x) || this.isGlobalPolicy(x), ); break; - case perUserPolicies: policies = policies.filter((x) => !!x.target.userName && !!x.target.host && !x.target.path); break; - case perHostPolicies: policies = policies.filter((x) => !x.target.userName && !!x.target.host && !x.target.path); break; - default: policies = policies.filter((x) => formatOwnerName(x.target) === this.state.selectedOwner); break; diff --git a/src/pages/Policy.jsx b/src/pages/Policy.tsx similarity index 100% rename from src/pages/Policy.jsx rename to src/pages/Policy.tsx diff --git a/src/pages/Preferences.jsx b/src/pages/Preferences.tsx similarity index 91% rename from src/pages/Preferences.jsx rename to src/pages/Preferences.tsx index 2fe976ab..aa7f7a6c 100644 --- a/src/pages/Preferences.jsx +++ b/src/pages/Preferences.tsx @@ -1,4 +1,4 @@ -import { useContext, React } from "react"; +import React, { useContext } from "react"; import Col from "react-bootstrap/Col"; import Container from "react-bootstrap/Container"; import Form from "react-bootstrap/Form"; @@ -6,7 +6,7 @@ import Row from "react-bootstrap/Row"; import Tab from "react-bootstrap/Tab"; import Tabs from "react-bootstrap/Tabs"; import { NotificationEditor } from "../components/notifications/NotificationEditor"; -import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; +import { Theme, UIPreferencesContext } from "../contexts/UIPreferencesContext"; /** * Class that exports preferences @@ -27,7 +27,7 @@ export function Preferences() { title="Select theme" id="themeSelector" value={theme} - onChange={(e) => setTheme(e.target.value)} + onChange={(e) => setTheme(e.target.value as Theme)} > @@ -55,7 +55,7 @@ export function Preferences() { className="form-select form-select-sm" title="Select byte representation" id="bytesBaseInput" - value={bytesStringBase2} + value={bytesStringBase2 ? "true" : "false"} onChange={(e) => setByteStringBase(e.target.value)} > diff --git a/src/pages/Repository.jsx b/src/pages/Repository.tsx similarity index 98% rename from src/pages/Repository.jsx rename to src/pages/Repository.tsx index 404fd5d7..3d36cdb9 100644 --- a/src/pages/Repository.jsx +++ b/src/pages/Repository.tsx @@ -15,8 +15,12 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import { faCheck, faChevronCircleDown, faChevronCircleUp, faWindowClose } from "@fortawesome/free-solid-svg-icons"; import { Logs } from "../components/Logs"; import { AppContext } from "../contexts/AppContext"; +import { ChangeEventHandle } from "src/components/types"; export class Repository extends Component { + handleChange: ChangeEventHandle; + mounted: boolean; + constructor() { super(); diff --git a/src/pages/SnapshotCreate.jsx b/src/pages/SnapshotCreate.tsx similarity index 96% rename from src/pages/SnapshotCreate.jsx rename to src/pages/SnapshotCreate.tsx index 7d735ebf..ceab9093 100644 --- a/src/pages/SnapshotCreate.jsx +++ b/src/pages/SnapshotCreate.tsx @@ -13,8 +13,12 @@ import { CLIEquivalent } from "../components/CLIEquivalent"; import { errorAlert, redirect } from "../utils/uiutil"; import { GoBackButton } from "../components/GoBackButton"; import PropTypes from "prop-types"; +import { ComponentChangeHandling, ChangeEventHandle } from "src/components/types"; + +class SnapshotCreateInternal extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + policyEditorRef: React.RefObject; -class SnapshotCreateInternal extends Component { constructor() { super(); this.state = { diff --git a/src/pages/SnapshotDirectory.jsx b/src/pages/SnapshotDirectory.tsx similarity index 100% rename from src/pages/SnapshotDirectory.jsx rename to src/pages/SnapshotDirectory.tsx diff --git a/src/pages/SnapshotHistory.jsx b/src/pages/SnapshotHistory.tsx similarity index 99% rename from src/pages/SnapshotHistory.jsx rename to src/pages/SnapshotHistory.tsx index a692c4f7..0150d757 100644 --- a/src/pages/SnapshotHistory.jsx +++ b/src/pages/SnapshotHistory.tsx @@ -75,7 +75,7 @@ class SnapshotHistoryInternal extends Component { } selectAll() { - let snapIds = {}; + const snapIds = {}; for (const sn of this.state.snapshots) { snapIds[sn.id] = true; } @@ -127,7 +127,7 @@ class SnapshotHistoryInternal extends Component { } deleteSelectedSnapshots() { - let req = { + const req = { source: { host: this.state.host, userName: this.state.userName, @@ -161,7 +161,7 @@ class SnapshotHistoryInternal extends Component { } deleteSnapshotSource() { - let req = { + const req = { source: { host: this.state.host, userName: this.state.userName, @@ -673,7 +673,7 @@ SnapshotHistoryInternal.propTypes = { navigate: PropTypes.func, }; -export function SnapshotHistory(props) { +export function SnapshotHistory(props: any) { const navigate = useNavigate(); const location = useLocation(); useContext(UIPreferencesContext); diff --git a/src/pages/SnapshotRestore.jsx b/src/pages/SnapshotRestore.tsx similarity index 80% rename from src/pages/SnapshotRestore.jsx rename to src/pages/SnapshotRestore.tsx index 106bf0f6..32e0b946 100644 --- a/src/pages/SnapshotRestore.jsx +++ b/src/pages/SnapshotRestore.tsx @@ -13,8 +13,58 @@ import { RequiredNumberField } from "../forms/RequiredNumberField"; import { errorAlert } from "../utils/uiutil"; import { GoBackButton } from "../components/GoBackButton"; import PropTypes from "prop-types"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; + +interface SnapshotRestoreRequest { + root: any; + options: { + incremental: boolean; + ignoreErrors: boolean; + restoreDirEntryAtDepth: number; + minSizeForPlaceholder: number; + }; + zipFile?: string; + uncompressedZip?: boolean; + tarFile?: string; + fsOutput?: { + targetPath: string; + skipOwners: boolean; + skipPermissions: boolean; + skipTimes: boolean; + ignorePermissionErrors: boolean; + overwriteFiles: boolean; + overwriteDirectories: boolean; + overwriteSymlinks: boolean; + writeFilesAtomically: boolean; + writeSparseFiles: boolean; + }; +} + +interface SnapshotRestoreInternalState { + incremental: boolean; + continueOnErrors: boolean; + restoreOwnership: boolean; + restorePermissions: boolean; + restoreModTimes: boolean; + uncompressedZip: boolean; + overwriteFiles: boolean; + overwriteDirectories: boolean; + overwriteSymlinks: boolean; + ignorePermissionErrors: boolean; + writeFilesAtomically: boolean; + writeSparseFiles: boolean; + restoreDirEntryAtDepth: number; + minSizeForPlaceholder: number; + restoreTask: string; + destination?: string; +} + +export class SnapshotRestoreInternal + extends Component + implements ComponentChangeHandling +{ + handleChange: ChangeEventHandle; -export class SnapshotRestoreInternal extends Component { constructor() { super(); @@ -40,7 +90,7 @@ export class SnapshotRestoreInternal extends Component { this.start = this.start.bind(this); } - start(e) { + start(e: React.FormEvent) { e.preventDefault(); if (!validateRequiredFields(this, ["destination"])) { @@ -49,7 +99,7 @@ export class SnapshotRestoreInternal extends Component { const dst = this.state.destination + ""; - let req = { + const req: SnapshotRestoreRequest = { root: this.props.params.oid, options: { incremental: this.state.incremental, @@ -97,7 +147,7 @@ export class SnapshotRestoreInternal extends Component { return (

- + Go To Restore Task . diff --git a/src/pages/Snapshots.jsx b/src/pages/Snapshots.tsx similarity index 93% rename from src/pages/Snapshots.jsx rename to src/pages/Snapshots.tsx index acd1496e..05b36d35 100644 --- a/src/pages/Snapshots.jsx +++ b/src/pages/Snapshots.tsx @@ -17,11 +17,15 @@ import { errorAlert, redirect, sizeWithFailures } from "../utils/uiutil"; import { policyEditorURL, sourceQueryStringParams } from "../utils/policyutil"; import { CLIEquivalent } from "../components/CLIEquivalent"; import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; const localSnapshots = "Local Snapshots"; const allSnapshots = "All Snapshots"; -export class Snapshots extends Component { +export class Snapshots extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + interval?: number; + constructor() { super(); this.state = { @@ -137,7 +141,7 @@ export class Snapshots extends Component { * @param x - the cell which content is changed * @returns - the content of the cell */ - statusCell(x, parent, bytesStringBase2) { + statusCell(x, parent, bytesStringBase2: boolean) { this.setHeader(x); switch (x.cell.getValue()) { case "IDLE": @@ -181,33 +185,23 @@ export class Snapshots extends Component { ); case "UPLOADING": { - let u = x.row.original.upload; + const u = x.row.original.upload; let title = ""; let totals = ""; if (u) { - title = - " hashed " + - u.hashedFiles + - " files (" + - sizeDisplayName(u.hashedBytes, bytesStringBase2) + - ")\n" + - " cached " + - u.cachedFiles + - " files (" + - sizeDisplayName(u.cachedBytes, bytesStringBase2) + - ")\n" + - " dir " + - u.directory; - const totalBytes = u.hashedBytes + u.cachedBytes; + const totalSize = sizeDisplayName(totalBytes, bytesStringBase2); + const hashedSize = sizeDisplayName(u.hashedBytes, bytesStringBase2); + const cachedSize = sizeDisplayName(u.cachedBytes, bytesStringBase2); - totals = sizeDisplayName(totalBytes, bytesStringBase2); + title = ` hashed ${u.hashedFiles} files (${hashedSize})\n cached ${u.cachedFiles} files (${cachedSize})\n dir ${u.directory}`; + totals = totalSize; if (u.estimatedBytes) { totals += "/" + sizeDisplayName(u.estimatedBytes, bytesStringBase2); const percent = Math.round((totalBytes * 1000.0) / u.estimatedBytes) / 10.0; if (percent <= 100) { - totals += " " + percent + "%"; + totals += ` ${percent}%`; } } } diff --git a/src/pages/Task.jsx b/src/pages/Task.tsx similarity index 98% rename from src/pages/Task.jsx rename to src/pages/Task.tsx index 10880b9e..0ed8ec9c 100644 --- a/src/pages/Task.jsx +++ b/src/pages/Task.tsx @@ -19,6 +19,8 @@ import { UIPreferencesContext } from "../contexts/UIPreferencesContext"; import PropTypes from "prop-types"; class TaskInternal extends Component { + interval: number | null; + constructor() { super(); this.state = { @@ -63,7 +65,7 @@ class TaskInternal extends Component { }); if (result.data.endTime) { - window.clearInterval(this.interval); + window.clearInterval(this.interval!); this.interval = null; } }) @@ -127,7 +129,7 @@ class TaskInternal extends Component { return 0; } - counterBadge(label, c) { + counterBadge(label: string, c) { const value = c?.value ?? 0; if (value <= this.valueThreshold()) { return ""; diff --git a/src/pages/Tasks.jsx b/src/pages/Tasks.tsx similarity index 95% rename from src/pages/Tasks.jsx rename to src/pages/Tasks.tsx index 610fdc03..dfd56406 100644 --- a/src/pages/Tasks.jsx +++ b/src/pages/Tasks.tsx @@ -13,8 +13,12 @@ import { handleChange } from "../forms"; import KopiaTable from "../components/KopiaTable"; import { redirect } from "../utils/uiutil"; import { taskStatusSymbol } from "../utils/taskutil"; +import { ChangeEventHandle, ComponentChangeHandling } from "src/components/types"; + +export class Tasks extends Component implements ComponentChangeHandling { + handleChange: ChangeEventHandle; + interval: number; -export class Tasks extends Component { constructor() { super(); this.state = { @@ -94,7 +98,7 @@ export class Tasks extends Component { return true; } - filterItems(items) { + filterItems(items: any[]) { return items.filter((c) => this.taskMatches(c)); } diff --git a/src/setupProxy.js b/src/setupProxy.ts similarity index 84% rename from src/setupProxy.js rename to src/setupProxy.ts index 94b3efd5..7675b0a3 100644 --- a/src/setupProxy.js +++ b/src/setupProxy.ts @@ -1,6 +1,6 @@ import { createProxyMiddleware } from "http-proxy-middleware"; -export default function (app) { +export default function (app: any) { app.use( "/api", createProxyMiddleware({ diff --git a/src/utils/deepstate.js b/src/utils/deepstate.ts similarity index 78% rename from src/utils/deepstate.js rename to src/utils/deepstate.ts index ec9a9c15..1a84ac75 100644 --- a/src/utils/deepstate.js +++ b/src/utils/deepstate.ts @@ -6,8 +6,8 @@ // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function setDeepStateProperty(component, name, value) { - let newState = { ...component.state }; +export function setDeepStateProperty(component: StateReadwrite, name: string, value: any): void { + const newState = { ...component.state }; let st = newState; const parts = name.split(/\./); @@ -30,12 +30,15 @@ export function setDeepStateProperty(component, name, value) { component.setState(newState); } +type StateReadwrite = { state: any; setState: (s: any) => void }; +type StateReadonly = { state: any }; + // getDeepStateProperty returns the provided deep state property or a default value // For example: { "a": { "b": { "c": true } } } // getDeepStateProperty("a") returns {b":{"c":true}} // getDeepStateProperty("a.b") returns {"c":true} // getDeepStateProperty("a.b.c") returns true -export function getDeepStateProperty(component, name, defaultValue = "") { +export function getDeepStateProperty(component: StateReadonly, name: string, defaultValue: any = ""): any { let st = component.state; const parts = name.split(/\./); diff --git a/src/utils/formatutils.js b/src/utils/formatutils.ts similarity index 80% rename from src/utils/formatutils.js rename to src/utils/formatutils.ts index cb65de00..6b55a2f0 100644 --- a/src/utils/formatutils.js +++ b/src/utils/formatutils.ts @@ -3,12 +3,12 @@ const locale = "en-US"; const base10UnitPrefixes = ["", "K", "M", "G", "T"]; const base2UnitPrefixes = ["", "Ki", "Mi", "Gi", "Ti"]; -function formatNumber(f) { +function formatNumber(f: number): string { return Math.round(f * 10) / 10.0 + ""; } -function toDecimalUnitString(f, thousand, prefixes, suffix) { - for (var i = 0; i < prefixes.length; i++) { +function toDecimalUnitString(f: number, thousand: number, prefixes: string[], suffix: string): string { + for (let i = 0; i < prefixes.length; i++) { if (f < 0.9 * thousand) { return formatNumber(f) + " " + prefixes[i] + suffix; } @@ -18,7 +18,7 @@ function toDecimalUnitString(f, thousand, prefixes, suffix) { return formatNumber(f) + " " + prefixes[prefixes.length - 1] + suffix; } -export function sizeDisplayName(size, bytesStringBase2) { +export function sizeDisplayName(size: number | undefined, bytesStringBase2: boolean = false): string { if (size === undefined) { return ""; } @@ -32,44 +32,38 @@ export function intervalDisplayName() { return "-"; } -export function timesOfDayDisplayName(v) { - if (!v) { - return "(none)"; - } - return v.length + " times"; -} - -export function parseQuery(queryString) { - var query = {}; - var pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&"); - for (var i = 0; i < pairs.length; i++) { - var pair = pairs[i].split("="); +type QueryParams = { [param: string]: string }; +export function parseQuery(queryString: string): QueryParams { + const query: QueryParams = {}; + const pairs = (queryString[0] === "?" ? queryString.substr(1) : queryString).split("&"); + for (let i = 0; i < pairs.length; i++) { + const pair = pairs[i].split("="); query[decodeURIComponent(pair[0])] = decodeURIComponent(pair[1] || ""); } return query; } -export function rfc3339TimestampForDisplay(n) { +export function rfc3339TimestampForDisplay(n: null | undefined | number | string | Date) { if (!n) { return ""; } - let t = new Date(n); + const t = new Date(n); return t.toLocaleString(); } -export function objectLink(n) { +export function objectLink(n: string) { if (n.startsWith("k") || n.startsWith("Ik")) { - return "/snapshots/dir/" + n; + return `/snapshots/dir/${n}`; } - return "/api/v1/objects/" + n; + return `/api/v1/objects/${n}`; } -export function formatOwnerName(s) { - return s.userName + "@" + s.host; +export function formatOwnerName(s: { userName: string; host: string }): string { + return `${s.userName}@${s.host}`; } -export function compare(a, b) { +export function compare(a: any, b: any): -1 | 0 | 1 { return a < b ? -1 : a > b ? 1 : 0; } @@ -81,12 +75,14 @@ export function compare(a, b) { * @param {number} ms - A duration (as a number of milliseconds). * @returns {string} A string representation of the duration. */ -export function formatMillisecondsUsingMultipleUnits(ms) { +export function formatMillisecondsUsingMultipleUnits(ms: number): string { const magnitudes = separateMillisecondsIntoMagnitudes(ms); const str = formatMagnitudesUsingMultipleUnits(magnitudes, true); return str; } +type Magnitudes = { days: number; hours: number; minutes: number; seconds: number; milliseconds: number }; + /** * Separate a duration into integer magnitudes of multiple units which, * when combined together, equal the original duration (minus any partial @@ -100,7 +96,7 @@ export function formatMillisecondsUsingMultipleUnits(ms) { * when combined together, represent the original duration * (minus any partial milliseconds). */ -export function separateMillisecondsIntoMagnitudes(ms) { +export function separateMillisecondsIntoMagnitudes(ms: number): Magnitudes { const magnitudes = { days: Math.trunc(ms / (1000 * 60 * 60 * 24)), hours: Math.trunc(ms / (1000 * 60 * 60)) % 24, @@ -133,7 +129,7 @@ export function separateMillisecondsIntoMagnitudes(ms) { * @param {boolean} abbreviateUnits - Whether you want to use short unit names. * @returns {string} Formatted string representing the specified duration. */ -export function formatMagnitudesUsingMultipleUnits(magnitudes, abbreviateUnits = false) { +export function formatMagnitudesUsingMultipleUnits(magnitudes: Magnitudes, abbreviateUnits = false) { // Define the label we will use for each unit, depending upon whether that // unit's magnitude is `1` or not (e.g. "0 minutes" vs. "1 minute"). // Note: This object is not used in the final "else" block below. @@ -196,7 +192,7 @@ export function formatMagnitudesUsingMultipleUnits(magnitudes, abbreviateUnits = * @param {boolean} useMultipleUnits - Whether you want to use multiple units. * @returns {string} The formatted string. */ -export function formatMilliseconds(ms, useMultipleUnits = false) { +export function formatMilliseconds(ms: number, useMultipleUnits = false): string { if (useMultipleUnits) { return formatMillisecondsUsingMultipleUnits(ms); } @@ -212,7 +208,11 @@ export function formatMilliseconds(ms, useMultipleUnits = false) { ); } -export function formatDuration(from, to, useMultipleUnits = false) { +export function formatDuration( + from: undefined | null | number | string | Date, + to?: number | string | Date, + useMultipleUnits = false, +) { if (!from) { return ""; } diff --git a/src/utils/policyutil.jsx b/src/utils/policyutil.tsx similarity index 67% rename from src/utils/policyutil.jsx rename to src/utils/policyutil.tsx index 015aeecf..72c19f6b 100644 --- a/src/utils/policyutil.jsx +++ b/src/utils/policyutil.tsx @@ -1,4 +1,4 @@ -export function isAbsolutePath(p) { +export function isAbsolutePath(p: string) { // Unix-style path. if (p.startsWith("/")) { return true; @@ -20,7 +20,7 @@ export function isAbsolutePath(p) { } // Refer to kopia/snapshot/source.go:ParseSourceInfo -export function checkPolicyPath(path) { +export function checkPolicyPath(path: string) { if (path === "(global)") { return "Cannot create the global policy, it already exists."; } @@ -52,7 +52,7 @@ export function checkPolicyPath(path) { return "Policies can not be defined for relative paths."; } -export function PolicyTypeName(s) { +export function PolicyTypeName(s: PolicyKey) { if (!s.host && !s.userName) { return "Global Policy"; } @@ -68,17 +68,20 @@ export function PolicyTypeName(s) { return "Directory: " + s.userName + "@" + s.host + ":" + s.path; } -export function sourceQueryStringParams(src) { - return ( - "userName=" + - encodeURIComponent(src.userName) + - "&host=" + - encodeURIComponent(src.host) + - "&path=" + - encodeURIComponent(src.path) - ); +export interface PolicyKey { + userName?: string; + host?: string; + path?: string; } -export function policyEditorURL(s) { - return "/policies/edit?" + sourceQueryStringParams(s); +export function sourceQueryStringParams(src: PolicyKey) { + // encodeURIComponent will in practice handle missing values too, but that is undefined behavior + const user = src.userName ? encodeURIComponent(src.userName) : ""; + const host = src.host ? encodeURIComponent(src.host) : ""; + const path = src.path ? encodeURIComponent(src.path) : ""; + return `userName=${user}&host=${host}&path=${path}`; +} + +export function policyEditorURL(s: PolicyKey) { + return `/policies/edit?${sourceQueryStringParams(s)}`; } diff --git a/src/utils/taskutil.jsx b/src/utils/taskutil.tsx similarity index 100% rename from src/utils/taskutil.jsx rename to src/utils/taskutil.tsx diff --git a/src/utils/uiutil.jsx b/src/utils/uiutil.tsx similarity index 68% rename from src/utils/uiutil.jsx rename to src/utils/uiutil.tsx index 8a31bafa..0ea094a9 100644 --- a/src/utils/uiutil.jsx +++ b/src/utils/uiutil.tsx @@ -3,7 +3,11 @@ import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React from "react"; import { sizeDisplayName } from "./formatutils.js"; -export function sizeWithFailures(size, summ, bytesStringBase2) { +export function sizeWithFailures( + size?: number, + summ?: { errors: { path: string; error: string }[]; numFailed: number } | null, + bytesStringBase2?: boolean, +): "" | React.JSX.Element { if (size === undefined) { return ""; } @@ -31,7 +35,7 @@ export function sizeWithFailures(size, summ, bytesStringBase2) { /** * In case of an error, redirect to the repository selection - * @param {error} The error that was returned + * @param {error} e The error that was returned */ export function redirect(e) { if (e && e.response && e.response.data && e.response.data.code === "NOT_CONNECTED") { @@ -39,7 +43,7 @@ export function redirect(e) { } } -export function errorAlert(err, prefix) { +export function errorAlert(err, prefix?) { if (!prefix) { prefix = "Error"; } @@ -55,19 +59,24 @@ export function errorAlert(err, prefix) { } } -export function toAlgorithmOption(x, defaultID) { - let text = x.id; +type Algorithm = { + id: string; + deprecated?: boolean; +}; - if (x.id === defaultID) { - text = x.id + " (RECOMMENDED)"; +export function toAlgorithmOption(algorithm: Algorithm, defaultID?) { + let text = algorithm.id; + + if (algorithm.id === defaultID) { + text = algorithm.id + " (RECOMMENDED)"; } - if (x.deprecated) { - text = x.id + " (NOT RECOMMENDED)"; + if (algorithm.deprecated) { + text = algorithm.id + " (NOT RECOMMENDED)"; } return ( - ); diff --git a/tests/App.test.jsx b/tests/App.test.tsx similarity index 99% rename from tests/App.test.jsx rename to tests/App.test.tsx index e2da9215..c4661bed 100644 --- a/tests/App.test.jsx +++ b/tests/App.test.tsx @@ -105,13 +105,13 @@ beforeEach(() => { axiosMock.onPut("/api/v1/ui-preferences").reply(200, {}); // Mock window.location for React Router - delete window.location; + window.location = undefined as any; window.location = { origin: "http://localhost:3000", href: "http://localhost:3000/", pathname: "/", replace: vi.fn(), - }; + } as any; }); afterEach(() => { diff --git a/tests/components/CLIEquivalent.test.jsx b/tests/components/CLIEquivalent.test.tsx similarity index 100% rename from tests/components/CLIEquivalent.test.jsx rename to tests/components/CLIEquivalent.test.tsx diff --git a/tests/components/DirectoryBreadcrumbs.test.jsx b/tests/components/DirectoryBreadcrumbs.test.tsx similarity index 98% rename from tests/components/DirectoryBreadcrumbs.test.jsx rename to tests/components/DirectoryBreadcrumbs.test.tsx index 6e646b68..4a471d2f 100644 --- a/tests/components/DirectoryBreadcrumbs.test.jsx +++ b/tests/components/DirectoryBreadcrumbs.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent, act } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; import "@testing-library/jest-dom"; import { BrowserRouter } from "react-router-dom"; -import { DirectoryBreadcrumbs } from "../../src/components/DirectoryBreadcrumbs"; +import { DirectoryBreadcrumbs } from "../../src/components/DirectoryBreadcrumbs.js"; import { mockNavigate, resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock react-router-dom using unified helper @@ -29,7 +29,7 @@ describe("DirectoryBreadcrumbs", () => { // Should render the Breadcrumb container but no items const breadcrumb = document.querySelector(".breadcrumb"); expect(breadcrumb).toBeInTheDocument(); - expect(breadcrumb.children).toHaveLength(0); + expect(breadcrumb!.children).toHaveLength(0); }); it("renders single breadcrumb item", () => { @@ -160,7 +160,7 @@ describe("DirectoryBreadcrumbs", () => { // Click on the info icon to show tooltip await act(async () => { - fireEvent.click(infoIcon); + fireEvent.click(infoIcon!); }); // Check for tooltip content diff --git a/tests/components/DirectoryItems.test.jsx b/tests/components/DirectoryItems.test.tsx similarity index 97% rename from tests/components/DirectoryItems.test.jsx rename to tests/components/DirectoryItems.test.tsx index b1615908..ded6500d 100644 --- a/tests/components/DirectoryItems.test.jsx +++ b/tests/components/DirectoryItems.test.tsx @@ -3,8 +3,8 @@ import { render, screen } from "@testing-library/react"; import { describe, it, expect, beforeEach, vi } from "vitest"; import "@testing-library/jest-dom"; import { BrowserRouter } from "react-router-dom"; -import { DirectoryItems } from "../../src/components/DirectoryItems"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; +import { DirectoryItems } from "../../src/components/DirectoryItems.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; // Mock react-router-dom Link component using unified helper vi.mock("react-router-dom", async () => { @@ -25,7 +25,7 @@ vi.mock("react-router-dom", async () => { // Helper function to render component with necessary providers const renderDirectoryItems = (props, contextOverrides = {}) => { - const defaultContext = { + const defaultContext: UIPreferences = { bytesStringBase2: false, pageSize: 10, theme: "light", @@ -256,7 +256,7 @@ describe("DirectoryItems", () => { const linkElement = dirLink.closest("a"); // Check that the link has the correct state data - const linkState = JSON.parse(linkElement.getAttribute("data-link-state")); + const linkState = JSON.parse(linkElement!.getAttribute("data-link-state")!); expect(linkState).toEqual({ label: "sub-folder", oid: "kdir888", diff --git a/tests/components/GoBackButton.test.jsx b/tests/components/GoBackButton.test.tsx similarity index 94% rename from tests/components/GoBackButton.test.jsx rename to tests/components/GoBackButton.test.tsx index 151724c1..c694bc95 100644 --- a/tests/components/GoBackButton.test.jsx +++ b/tests/components/GoBackButton.test.tsx @@ -3,7 +3,7 @@ import { render, screen, fireEvent } from "@testing-library/react"; import { BrowserRouter } from "react-router-dom"; import { vi } from "vitest"; import "@testing-library/jest-dom"; -import { GoBackButton } from "../../src/components/GoBackButton"; +import { GoBackButton } from "../../src/components/GoBackButton.js"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock react-router-dom using the unified helper diff --git a/tests/components/KopiaTable.test.jsx b/tests/components/KopiaTable.test.tsx similarity index 96% rename from tests/components/KopiaTable.test.jsx rename to tests/components/KopiaTable.test.tsx index adaddc81..7060bfaf 100644 --- a/tests/components/KopiaTable.test.jsx +++ b/tests/components/KopiaTable.test.tsx @@ -5,21 +5,24 @@ import { expect, describe, it, vi, beforeEach } from "vitest"; import "@testing-library/jest-dom"; import KopiaTable from "../../src/components/KopiaTable"; -import { UIPreferencesContext, PAGE_SIZES } from "../../src/contexts/UIPreferencesContext"; +import { UIPreferencesContext, PAGE_SIZES, UIPreferences, PageSize } from "../../src/contexts/UIPreferencesContext"; // Test data and mock setup -const createMockContext = (pageSize = 10) => ({ - pageSize, - setPageSize: vi.fn(), - theme: "light", - bytesStringBase2: false, - defaultSnapshotViewAll: false, - fontSize: "fs-6", - setTheme: vi.fn(), - setByteStringBase: vi.fn(), - setDefaultSnapshotViewAll: vi.fn(), - setFontSize: vi.fn(), -}); +function createMockContext(pageSize: PageSize = 10) { + const pref: UIPreferences = { + pageSize, + setPageSize: vi.fn(), + theme: "light", + bytesStringBase2: false, + defaultSnapshotViewAll: false, + fontSize: "fs-6", + setTheme: vi.fn(), + setByteStringBase: vi.fn(), + setDefaultSnapshotViewAll: vi.fn(), + setFontSize: vi.fn(), + }; + return pref; +} const sampleColumns = [ { diff --git a/tests/components/Logs.test.jsx b/tests/components/Logs.test.tsx similarity index 97% rename from tests/components/Logs.test.jsx rename to tests/components/Logs.test.tsx index b79322e6..a2f8124f 100644 --- a/tests/components/Logs.test.jsx +++ b/tests/components/Logs.test.tsx @@ -101,7 +101,7 @@ describe("Logs Component", () => { // The component formats time as HH:MM:SS.mmm // For timestamp 1672531200.123, depending on timezone const timeElement = screen.getByText(/Test message/).parentElement; - expect(timeElement.textContent).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/); + expect(timeElement!.textContent).toMatch(/\d{2}:\d{2}:\d{2}\.\d{3}/); }); }); @@ -207,7 +207,7 @@ describe("Logs Component", () => { }); // Clear the mock calls from initial render - Element.prototype.scrollIntoView.mockClear(); + (Element.prototype.scrollIntoView as jest.Mock).mockClear(); // Trigger the interval callback manually await triggerIntervals(); @@ -241,7 +241,7 @@ describe("Logs Component", () => { const initialCallCount = callCount; // Clear the mock calls from initial render - Element.prototype.scrollIntoView.mockClear(); + (Element.prototype.scrollIntoView as jest.Mock).mockClear(); // Trigger the interval callback manually await triggerIntervals(); @@ -307,7 +307,7 @@ describe("Logs Component", () => { const cell = container.querySelector("td.elide"); expect(cell).toBeTruthy(); - const title = cell.getAttribute("title"); + const title = cell!.getAttribute("title"); expect(title).toBeTruthy(); expect(title).toMatch(/\d{1,2}\/\d{1,2}\/\d{4}/); }); diff --git a/tests/components/PolicyEditor.test.jsx b/tests/components/PolicyEditor.test.tsx similarity index 92% rename from tests/components/PolicyEditor.test.jsx rename to tests/components/PolicyEditor.test.tsx index d477844e..0673a82c 100644 --- a/tests/components/PolicyEditor.test.jsx +++ b/tests/components/PolicyEditor.test.tsx @@ -118,8 +118,8 @@ it("e2e", async () => { , ); - await waitFor(() => expect(getByTestId("effective-retention.keepHourly").value).toBe("45")); - expect(getByTestId("effective-retention.keepLatest").value).toEqual("33"); + await waitFor(() => expect((getByTestId("effective-retention.keepHourly") as HTMLInputElement).value).toBe("45")); + expect((getByTestId("effective-retention.keepLatest") as HTMLInputElement).value).toEqual("33"); await waitFor(() => expect(getByTestId("definition-retention.keepLatest").innerHTML).toContain(`Directory: some-user@h1:some-path`), @@ -139,7 +139,7 @@ it("e2e", async () => { fireEvent.change(getByTestId("control-policy.retention.keepLatest"), { target: { value: "44" } }); // this will trigger resolve and will update effective field: "(Defined by this policy)" - await waitFor(() => expect(getByTestId("effective-retention.keepLatest").value).toBe("44")); + await waitFor(() => expect((getByTestId("effective-retention.keepLatest") as HTMLInputElement).value).toBe("44")); await waitFor(() => expect(getByTestId("definition-retention.keepLatest").innerHTML).toEqual("(Defined by this policy)"), ); diff --git a/tests/components/PolicyEditorLink.test.jsx b/tests/components/PolicyEditorLink.test.tsx similarity index 96% rename from tests/components/PolicyEditorLink.test.jsx rename to tests/components/PolicyEditorLink.test.tsx index 449c1382..3b3566c0 100644 --- a/tests/components/PolicyEditorLink.test.jsx +++ b/tests/components/PolicyEditorLink.test.tsx @@ -10,7 +10,8 @@ vi.mock("react-router-dom", async () => { return createRouterMock()(); }); -import { PolicyEditorLink } from "../../src/components/PolicyEditorLink"; +import { PolicyEditorLink } from "../../src/components/PolicyEditorLink.js"; +import { PolicyKey } from "../../src/utils/policyutil.js"; // Helper to render components with router const renderWithRouter = (component) => { @@ -19,7 +20,7 @@ const renderWithRouter = (component) => { describe("PolicyEditorLink", () => { it("renders link with correct URL and text", () => { - const source = { + const source: PolicyKey = { userName: "john", host: "example.com", path: "/home/john", diff --git a/tests/components/SetupRepository.test.jsx b/tests/components/SetupRepository.test.tsx similarity index 100% rename from tests/components/SetupRepository.test.jsx rename to tests/components/SetupRepository.test.tsx diff --git a/tests/components/SetupRepositoryAzure.test.jsx b/tests/components/SetupRepositoryAzure.test.tsx similarity index 82% rename from tests/components/SetupRepositoryAzure.test.jsx rename to tests/components/SetupRepositoryAzure.test.tsx index 2da8797c..ca046793 100644 --- a/tests/components/SetupRepositoryAzure.test.jsx +++ b/tests/components/SetupRepositoryAzure.test.tsx @@ -4,22 +4,22 @@ import { SetupRepositoryAzure } from "../../src/components/SetupRepositoryAzure" import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-container"), { target: { value: "some-container" } }); fireEvent.change(getByTestId("control-storageAccount"), { target: { value: "some-storageAccount" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-storageKey"), { target: { value: "some-storageKey" } }); fireEvent.change(getByTestId("control-sasToken"), { target: { value: "some-sas-token" } }); fireEvent.change(getByTestId("control-storageDomain"), { target: { value: "some-storage-domain" } }); fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ storageAccount: "some-storageAccount", container: "some-container", prefix: "some-prefix", diff --git a/tests/components/SetupRepositoryB2.test.jsx b/tests/components/SetupRepositoryB2.test.tsx similarity index 76% rename from tests/components/SetupRepositoryB2.test.jsx rename to tests/components/SetupRepositoryB2.test.tsx index a3a21445..5383b649 100644 --- a/tests/components/SetupRepositoryB2.test.jsx +++ b/tests/components/SetupRepositoryB2.test.tsx @@ -4,20 +4,20 @@ import { SetupRepositoryB2 } from "../../src/components/SetupRepositoryB2"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); fireEvent.change(getByTestId("control-keyId"), { target: { value: "some-key-id" } }); fireEvent.change(getByTestId("control-key"), { target: { value: "some-key" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ bucket: "some-bucket", keyId: "some-key-id", key: "some-key", diff --git a/tests/components/SetupRepositoryFilesystem.test.jsx b/tests/components/SetupRepositoryFilesystem.test.tsx similarity index 69% rename from tests/components/SetupRepositoryFilesystem.test.jsx rename to tests/components/SetupRepositoryFilesystem.test.tsx index d051e506..f3dfd3c9 100644 --- a/tests/components/SetupRepositoryFilesystem.test.jsx +++ b/tests/components/SetupRepositoryFilesystem.test.tsx @@ -4,15 +4,15 @@ import { SetupRepositoryFilesystem } from "../../src/components/SetupRepositoryF import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-path"), { target: { value: "some-path" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ path: "some-path", }); }); diff --git a/tests/components/SetupRepositoryGCS.test.jsx b/tests/components/SetupRepositoryGCS.test.tsx similarity index 77% rename from tests/components/SetupRepositoryGCS.test.jsx rename to tests/components/SetupRepositoryGCS.test.tsx index 19423e26..57f166ed 100644 --- a/tests/components/SetupRepositoryGCS.test.jsx +++ b/tests/components/SetupRepositoryGCS.test.tsx @@ -4,20 +4,20 @@ import { SetupRepositoryGCS } from "../../src/components/SetupRepositoryGCS"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); fireEvent.change(getByTestId("control-credentialsFile"), { target: { value: "some-credentials-file" } }); fireEvent.change(getByTestId("control-credentials"), { target: { value: "some-credentials" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ bucket: "some-bucket", credentials: "some-credentials", credentialsFile: "some-credentials-file", diff --git a/tests/components/SetupRepositoryRclone.test.jsx b/tests/components/SetupRepositoryRclone.test.tsx similarity index 71% rename from tests/components/SetupRepositoryRclone.test.jsx rename to tests/components/SetupRepositoryRclone.test.tsx index 16222b3f..b85caea4 100644 --- a/tests/components/SetupRepositoryRclone.test.jsx +++ b/tests/components/SetupRepositoryRclone.test.tsx @@ -4,18 +4,18 @@ import { SetupRepositoryRclone } from "../../src/components/SetupRepositoryRclon import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-remotePath"), { target: { value: "myremote:path/to/repo" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-rcloneExe"), { target: { value: "/usr/bin/rclone" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ remotePath: "myremote:path/to/repo", rcloneExe: "/usr/bin/rclone", }); diff --git a/tests/components/SetupRepositoryS3.test.jsx b/tests/components/SetupRepositoryS3.test.tsx similarity index 80% rename from tests/components/SetupRepositoryS3.test.jsx rename to tests/components/SetupRepositoryS3.test.tsx index 3d7094d9..7a97f9b8 100644 --- a/tests/components/SetupRepositoryS3.test.jsx +++ b/tests/components/SetupRepositoryS3.test.tsx @@ -4,25 +4,25 @@ import { SetupRepositoryS3 } from "../../src/components/SetupRepositoryS3"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-bucket"), { target: { value: "some-bucket" } }); fireEvent.change(getByTestId("control-accessKeyID"), { target: { value: "some-accessKeyID" } }); fireEvent.change(getByTestId("control-secretAccessKey"), { target: { value: "some-secretAccessKey" } }); fireEvent.change(getByTestId("control-endpoint"), { target: { value: "some-endpoint" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // optional fireEvent.click(getByTestId("control-doNotUseTLS")); fireEvent.click(getByTestId("control-doNotVerifyTLS")); fireEvent.change(getByTestId("control-prefix"), { target: { value: "some-prefix" } }); fireEvent.change(getByTestId("control-sessionToken"), { target: { value: "some-sessionToken" } }); fireEvent.change(getByTestId("control-region"), { target: { value: "some-region" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ accessKeyID: "some-accessKeyID", bucket: "some-bucket", endpoint: "some-endpoint", @@ -36,6 +36,6 @@ it("can set fields", async () => { fireEvent.click(getByTestId("control-doNotUseTLS")); fireEvent.click(getByTestId("control-doNotVerifyTLS")); - expect(ref.current.state.doNotUseTLS).toBe(false); - expect(ref.current.state.doNotVerifyTLS).toBe(false); + expect(ref.current!.state.doNotUseTLS).toBe(false); + expect(ref.current!.state.doNotVerifyTLS).toBe(false); }); diff --git a/tests/components/SetupRepositorySFTP.test.jsx b/tests/components/SetupRepositorySFTP.test.tsx similarity index 77% rename from tests/components/SetupRepositorySFTP.test.jsx rename to tests/components/SetupRepositorySFTP.test.tsx index e28ba7a5..abe981fb 100644 --- a/tests/components/SetupRepositorySFTP.test.jsx +++ b/tests/components/SetupRepositorySFTP.test.tsx @@ -4,10 +4,10 @@ import { SetupRepositorySFTP } from "../../src/components/SetupRepositorySFTP"; import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-host"), { target: { value: "some-host" } }); fireEvent.change(getByTestId("control-port"), { target: { value: "22" } }); @@ -15,10 +15,10 @@ it("can set fields", async () => { fireEvent.change(getByTestId("control-username"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-keyfile"), { target: { value: "some-keyfile" } }); fireEvent.change(getByTestId("control-knownHostsFile"), { target: { value: "some-knownHostsFile" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // key file + known hosts file - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", keyfile: "some-keyfile", @@ -30,12 +30,12 @@ it("can set fields", async () => { // now enter key data instead of key file, make sure validation triggers along the way fireEvent.change(getByTestId("control-keyData"), { target: { value: "some-keyData" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-keyfile"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // key data + known hosts file - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", keyfile: "", @@ -47,17 +47,17 @@ it("can set fields", async () => { }); fireEvent.change(getByTestId("control-password"), { target: { value: "some-password" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-keyData"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); fireEvent.change(getByTestId("control-knownHostsData"), { target: { value: "some-knownHostsData" } }); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); fireEvent.change(getByTestId("control-knownHostsFile"), { target: { value: "" } }); - act(() => expect(ref.current.validate()).toBe(true)); + act(() => expect(ref.current!.validate()).toBe(true)); // known hosts data + password - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ host: "some-host", username: "some-username", password: "some-password", diff --git a/tests/components/SetupRepositoryServer.test.jsx b/tests/components/SetupRepositoryServer.test.tsx similarity index 73% rename from tests/components/SetupRepositoryServer.test.jsx rename to tests/components/SetupRepositoryServer.test.tsx index 440a50de..eaded96d 100644 --- a/tests/components/SetupRepositoryServer.test.jsx +++ b/tests/components/SetupRepositoryServer.test.tsx @@ -4,18 +4,18 @@ import { SetupRepositoryServer } from "../../src/components/SetupRepositoryServe import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-url"), { target: { value: "https://kopia.example.com:51515" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-serverCertFingerprint"), { target: { value: "sha256:abcd1234567890" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ url: "https://kopia.example.com:51515", serverCertFingerprint: "sha256:abcd1234567890", }); diff --git a/tests/components/SetupRepositoryToken.test.jsx b/tests/components/SetupRepositoryToken.test.tsx similarity index 69% rename from tests/components/SetupRepositoryToken.test.jsx rename to tests/components/SetupRepositoryToken.test.tsx index dacb1723..5b56cb6d 100644 --- a/tests/components/SetupRepositoryToken.test.jsx +++ b/tests/components/SetupRepositoryToken.test.tsx @@ -4,15 +4,15 @@ import { SetupRepositoryToken } from "../../src/components/SetupRepositoryToken" import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-token"), { target: { value: "some-token" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ token: "some-token", }); }); diff --git a/tests/components/SetupRepositoryWebDAV.test.jsx b/tests/components/SetupRepositoryWebDAV.test.tsx similarity index 74% rename from tests/components/SetupRepositoryWebDAV.test.jsx rename to tests/components/SetupRepositoryWebDAV.test.tsx index 30b91d47..c6a474b1 100644 --- a/tests/components/SetupRepositoryWebDAV.test.jsx +++ b/tests/components/SetupRepositoryWebDAV.test.tsx @@ -4,21 +4,21 @@ import { SetupRepositoryWebDAV } from "../../src/components/SetupRepositoryWebDA import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-url"), { target: { value: "some-url" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-username"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-password"), { target: { value: "some-password" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ url: "some-url", username: "some-username", password: "some-password", diff --git a/tests/components/SnapshotEstimation.test.jsx b/tests/components/SnapshotEstimation.test.tsx similarity index 95% rename from tests/components/SnapshotEstimation.test.jsx rename to tests/components/SnapshotEstimation.test.tsx index cd6e257d..c50d9f9e 100644 --- a/tests/components/SnapshotEstimation.test.jsx +++ b/tests/components/SnapshotEstimation.test.tsx @@ -3,13 +3,17 @@ import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; -import { SnapshotEstimation } from "../../src/components/SnapshotEstimation"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotEstimation } from "../../src/components/SnapshotEstimation.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; import PropTypes from "prop-types"; -import { setupIntervalMocks, cleanupIntervalMocks, waitForLoadAndTriggerIntervals } from "../testutils/interval-mocks"; +import { + setupIntervalMocks, + cleanupIntervalMocks, + waitForLoadAndTriggerIntervals, +} from "../testutils/interval-mocks.js"; // Mock Logs component to avoid complex dependencies vi.mock("../../src/components/Logs", () => ({ @@ -49,18 +53,20 @@ vi.mock("../../src/utils/taskutil", async () => { }); // Create mock UI preferences context -const createMockUIContext = () => ({ - pageSize: 10, - bytesStringBase2: false, - defaultSnapshotViewAll: false, - theme: "light", - fontSize: "fs-6", - setTheme: vi.fn(), - setPageSize: vi.fn(), - setByteStringBase: vi.fn(), - setDefaultSnapshotViewAll: vi.fn(), - setFontSize: vi.fn(), -}); +function createMockUIContext(): UIPreferences { + return { + pageSize: 10, + bytesStringBase2: false, + defaultSnapshotViewAll: false, + theme: "light", + fontSize: "fs-6", + setTheme: vi.fn(), + setPageSize: vi.fn(), + setByteStringBase: vi.fn(), + setDefaultSnapshotViewAll: vi.fn(), + setFontSize: vi.fn(), + }; +} // Mock server let serverMock; @@ -266,8 +272,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = false; + const uiContext = { ...createMockUIContext(), bytesStringBase2: false }; renderWithProviders(, uiContext); @@ -298,8 +303,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = true; + const uiContext = { ...createMockUIContext(), bytesStringBase2: true }; renderWithProviders(, uiContext); @@ -368,7 +372,7 @@ describe("SnapshotEstimation", () => { describe("Cancel task functionality", () => { it("calls cancelTask when cancel button is clicked", async () => { - const { cancelTask } = await import("../../src/utils/taskutil"); + const { cancelTask } = await import("../../src/utils/taskutil.js"); const runningTask = { id: "test-task-id", @@ -592,7 +596,7 @@ describe("SnapshotEstimation", () => { describe("Error handling edge cases", () => { it("handles redirect on API error", async () => { - const { redirect } = await import("../../src/utils/uiutil"); + const { redirect } = await import("../../src/utils/uiutil.js"); serverMock.onGet("/api/v1/tasks/test-task-id").reply(401, { code: "NOT_CONNECTED", @@ -650,8 +654,7 @@ describe("SnapshotEstimation", () => { serverMock.onGet("/api/v1/tasks/test-task-id").reply(200, task); - const uiContext = createMockUIContext(); - uiContext.bytesStringBase2 = false; // Start with base 10 + const uiContext = { ...createMockUIContext(), bytesStringBase2: false }; const { rerender } = renderWithProviders(, uiContext); diff --git a/tests/components/notifications/EmailNotificationMethod.test.jsx b/tests/components/notifications/EmailNotificationMethod.test.tsx similarity index 83% rename from tests/components/notifications/EmailNotificationMethod.test.jsx rename to tests/components/notifications/EmailNotificationMethod.test.tsx index 0f68bb33..3dc22ec9 100644 --- a/tests/components/notifications/EmailNotificationMethod.test.jsx +++ b/tests/components/notifications/EmailNotificationMethod.test.tsx @@ -4,23 +4,23 @@ import { EmailNotificationMethod } from "../../../src/components/notifications/E import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-smtpServer"), { target: { value: "some-smtpServer" } }); fireEvent.change(getByTestId("control-smtpPort"), { target: { value: 25 } }); fireEvent.change(getByTestId("control-from"), { target: { value: "some-from@example.com" } }); fireEvent.change(getByTestId("control-to"), { target: { value: "some-to@example.com" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-smtpUsername"), { target: { value: "some-username" } }); fireEvent.change(getByTestId("control-smtpPassword"), { target: { value: "some-password" } }); fireEvent.change(getByTestId("control-smtpIdentity"), { target: { value: "some-identity" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ smtpServer: "some-smtpServer", smtpPort: 25, smtpUsername: "some-username", diff --git a/tests/components/notifications/NotificationEditor.test.jsx b/tests/components/notifications/NotificationEditor.test.tsx similarity index 100% rename from tests/components/notifications/NotificationEditor.test.jsx rename to tests/components/notifications/NotificationEditor.test.tsx diff --git a/tests/components/notifications/PushoverNotificationMethod.test.jsx b/tests/components/notifications/PushoverNotificationMethod.test.tsx similarity index 72% rename from tests/components/notifications/PushoverNotificationMethod.test.jsx rename to tests/components/notifications/PushoverNotificationMethod.test.tsx index 51640dfe..c30fda40 100644 --- a/tests/components/notifications/PushoverNotificationMethod.test.jsx +++ b/tests/components/notifications/PushoverNotificationMethod.test.tsx @@ -4,18 +4,18 @@ import { PushoverNotificationMethod } from "../../../src/components/notification import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-appToken"), { target: { value: "some-appToken" } }); fireEvent.change(getByTestId("control-userKey"), { target: { value: "some-userKey" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ appToken: "some-appToken", userKey: "some-userKey", format: "txt", diff --git a/tests/components/notifications/WebHookNotificationMethod.test.jsx b/tests/components/notifications/WebHookNotificationMethod.test.tsx similarity index 74% rename from tests/components/notifications/WebHookNotificationMethod.test.jsx rename to tests/components/notifications/WebHookNotificationMethod.test.tsx index 4fb0f75e..1551caa4 100644 --- a/tests/components/notifications/WebHookNotificationMethod.test.jsx +++ b/tests/components/notifications/WebHookNotificationMethod.test.tsx @@ -4,18 +4,18 @@ import { WebHookNotificationMethod } from "../../../src/components/notifications import { fireEvent } from "@testing-library/react"; it("can set fields", async () => { - let ref = React.createRef(); + let ref = React.createRef(); const { getByTestId } = render(); - act(() => expect(ref.current.validate()).toBe(false)); + act(() => expect(ref.current!.validate()).toBe(false)); // required fireEvent.change(getByTestId("control-endpoint"), { target: { value: "http://some-endpoint:12345" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); // optional fireEvent.change(getByTestId("control-headers"), { target: { value: "some:header\nanother:header" } }); - expect(ref.current.validate()).toBe(true); + expect(ref.current!.validate()).toBe(true); - expect(ref.current.state).toStrictEqual({ + expect(ref.current!.state).toStrictEqual({ endpoint: "http://some-endpoint:12345", method: "POST", format: "txt", diff --git a/tests/contexts/UIPreferencesContext.test.jsx b/tests/contexts/UIPreferencesContext.test.tsx similarity index 94% rename from tests/contexts/UIPreferencesContext.test.jsx rename to tests/contexts/UIPreferencesContext.test.tsx index 253b81f3..ad7bfce1 100644 --- a/tests/contexts/UIPreferencesContext.test.jsx +++ b/tests/contexts/UIPreferencesContext.test.tsx @@ -22,13 +22,13 @@ afterEach(() => { }); // Helper component to access context values -const TestComponent = ({ onMount }) => { +function TestComponent({ onMount }) { const preferences = useContext(UIPreferencesContext); React.useEffect(() => { onMount(preferences); }, [preferences, onMount]); return null; -}; +} describe("UIPreferencesContext", () => { describe("Default values", () => { @@ -41,7 +41,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -74,7 +74,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -100,7 +100,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -129,7 +129,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -162,7 +162,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; const { unmount } = render( - + (capturedPreferences = prefs)} /> , ); @@ -191,7 +191,7 @@ describe("UIPreferencesContext", () => { it("should update theme and sync with HTML classes", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -219,7 +219,7 @@ describe("UIPreferencesContext", () => { it("should update font size and sync with HTML classes", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -242,7 +242,7 @@ describe("UIPreferencesContext", () => { it("should update page size", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -263,7 +263,7 @@ describe("UIPreferencesContext", () => { it("should update bytesStringBase2", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -292,7 +292,7 @@ describe("UIPreferencesContext", () => { it("should update defaultSnapshotViewAll", async () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); @@ -337,7 +337,7 @@ describe("UIPreferencesContext", () => { axiosMock.onPut("/api/v1/ui-preferences").reply(200); render( - + {}} /> , ); @@ -371,7 +371,7 @@ describe("UIPreferencesContext", () => { let capturedPreferences; render( - + (capturedPreferences = prefs)} /> , ); diff --git a/tests/forms/StringList.test.jsx b/tests/forms/StringList.test.tsx similarity index 74% rename from tests/forms/StringList.test.jsx rename to tests/forms/StringList.test.tsx index beb86076..51e111bd 100644 --- a/tests/forms/StringList.test.jsx +++ b/tests/forms/StringList.test.tsx @@ -1,18 +1,22 @@ import { render, act } from "@testing-library/react"; import React from "react"; -import PropTypes from "prop-types"; import { StringList } from "../../src/forms/StringList"; import { fireEvent } from "@testing-library/react"; import { listToMultilineString, multilineStringToList } from "../../src/forms/StringList"; // Mock component to simulate the form component that would use StringList -class MockFormComponent extends React.Component { - static propTypes = { - fieldName: PropTypes.string.isRequired, - initialState: PropTypes.object, - props: PropTypes.object, - }; +interface MockFormComponentProps { + fieldName: string; + initialState?: Record; + props?: Record; +} + +interface MockFormComponentState { + testField: unknown; + [key: string]: unknown; // This is here for handleChange to work with dynamic field names +} +class MockFormComponent extends React.Component { constructor(props) { super(props); this.state = props.initialState || {}; @@ -58,32 +62,32 @@ describe("listToMultilineString", () => { describe("multilineStringToList", () => { it("converts multiline string to array", () => { - const target = { value: "first\nsecond\nthird" }; + const target = { value: "first\nsecond\nthird" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "second", "third"]); }); it("returns undefined for empty string", () => { - const target = { value: "" }; + const target = { value: "" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toBeUndefined(); }); it("handles single line string", () => { - const target = { value: "single line" }; + const target = { value: "single line" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["single line"]); }); it("handles strings with empty lines", () => { - const target = { value: "first\n\nthird" }; + const target = { value: "first\n\nthird" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "", "third"]); }); it("handles strings with carriage returns", () => { - const target = { value: "first\r\nsecond" }; + const target = { value: "first\r\nsecond" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first\r", "second"]); }); it("handles trailing newline", () => { - const target = { value: "first\nsecond\n" }; + const target = { value: "first\nsecond\n" } as HTMLTextAreaElement; expect(multilineStringToList(target)).toEqual(["first", "second", ""]); }); }); @@ -92,7 +96,7 @@ describe("StringList component", () => { it("renders empty textarea when no value is set", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe(""); expect(textarea.name).toBe("testField"); }); @@ -104,25 +108,25 @@ describe("StringList component", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("item 1\nitem 2\nitem 3"); }); it("handles onChange event and updates state", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "new item 1\nnew item 2" } }); }); - expect(ref.current.state.testField).toEqual(["new item 1", "new item 2"]); + expect(ref.current!.state.testField).toEqual(["new item 1", "new item 2"]); }); it("clears state when input is empty", () => { - let ref = React.createRef(); + const ref = React.createRef(); const initialState = { testField: ["existing", "items"], }; @@ -135,13 +139,13 @@ describe("StringList component", () => { fireEvent.change(textarea, { target: { value: "" } }); }); - expect(ref.current.state.testField).toBeUndefined(); + expect(ref.current!.state.testField).toBeUndefined(); }); it("has correct textarea attributes", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.tagName).toBe("TEXTAREA"); expect(textarea.rows).toBe(5); expect(textarea.classList.contains("form-control-sm")).toBe(true); @@ -157,27 +161,27 @@ describe("StringList component", () => { const { getByTestId } = render(); - const textarea = getByTestId("string-list-input"); + const textarea = getByTestId("string-list-input") as HTMLTextAreaElement; expect(textarea.placeholder).toBe("Enter items"); expect(textarea.disabled).toBe(true); expect(textarea.classList.contains("custom-class")).toBe(true); }); it("handles component state updates", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render( , ); - let textarea = getByRole("textbox"); + let textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("initial"); // Update the component state act(() => { - ref.current.setState({ testField: ["updated", "list"] }); + ref.current!.setState({ testField: ["updated", "list"] }); }); - textarea = getByRole("textbox"); + textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("updated\nlist"); }); @@ -190,20 +194,20 @@ describe("StringList component", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("nested\nvalue"); }); it("maintains proper Form.Group structure", () => { const { container } = render(); - const formGroup = container.querySelector(".col"); + const formGroup = container.querySelector(".col")!; expect(formGroup).toBeTruthy(); expect(formGroup.querySelector("textarea.form-control")).toBeTruthy(); }); it("preserves whitespace in list items", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -212,6 +216,6 @@ describe("StringList component", () => { fireEvent.change(textarea, { target: { value: " item with spaces \n\ttabbed item\n normal item" } }); }); - expect(ref.current.state.testField).toEqual([" item with spaces ", "\ttabbed item", " normal item"]); + expect(ref.current!.state.testField).toEqual([" item with spaces ", "\ttabbed item", " normal item"]); }); }); diff --git a/tests/forms/TimesOfDayList.test.jsx b/tests/forms/TimesOfDayList.test.tsx similarity index 76% rename from tests/forms/TimesOfDayList.test.jsx rename to tests/forms/TimesOfDayList.test.tsx index 34836368..0411542f 100644 --- a/tests/forms/TimesOfDayList.test.jsx +++ b/tests/forms/TimesOfDayList.test.tsx @@ -4,8 +4,19 @@ import PropTypes from "prop-types"; import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; import { fireEvent } from "@testing-library/react"; +interface MockFormComponentProps { + fieldName: string; + initialState?: Record; + props?: Record; +} + +interface MockFormComponentState { + testField: unknown; + [key: string]: unknown; // This is here for handleChange to work with dynamic field names +} + // Mock component to simulate the form component that would use TimesOfDayList -class MockFormComponent extends React.Component { +class MockFormComponent extends React.Component { static propTypes = { fieldName: PropTypes.string.isRequired, initialState: PropTypes.object, @@ -33,7 +44,7 @@ describe("TimesOfDayList", () => { it("renders empty textarea when no times are set", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe(""); }); @@ -48,7 +59,7 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30\n15:45\n23:00"); }); @@ -59,12 +70,12 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30\ninvalid-time\n15:45"); }); it("parses valid time strings correctly", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -73,7 +84,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "9:30\n15:45\n23:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, { hour: 15, min: 45 }, { hour: 23, min: 0 }, @@ -81,7 +92,7 @@ describe("TimesOfDayList", () => { }); it("handles mixed valid and invalid time strings", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -90,7 +101,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "9:30\ninvalid\n15:45\n25:00\n12:5" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, "invalid", { hour: 15, min: 45 }, @@ -100,7 +111,7 @@ describe("TimesOfDayList", () => { }); it("validates hour range (0-23)", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -109,7 +120,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "0:00\n23:59\n24:00\n-1:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 0, min: 0 }, { hour: 23, min: 59 }, "24:00", // Invalid - hour >= 24 @@ -118,16 +129,16 @@ describe("TimesOfDayList", () => { }); it("validates minute range (0-59)", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "12:00\n12:59\n12:60\n12:-1" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 12, min: 0 }, { hour: 12, min: 59 }, "12:60", // Invalid - minute >= 60 @@ -136,16 +147,16 @@ describe("TimesOfDayList", () => { }); it("requires two-digit minute format for single digits", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "12:05\n12:5\n12:00\n12:9" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 12, min: 5 }, // Valid - two digits "12:5", // Invalid - single digit minute { hour: 12, min: 0 }, // Valid - 00 is two digits @@ -154,7 +165,7 @@ describe("TimesOfDayList", () => { }); it("handles empty string input", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -163,20 +174,20 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "" } }); }); - expect(ref.current.state.testField).toBeUndefined(); + expect(ref.current!.state.testField).toBeUndefined(); }); it("handles whitespace and empty lines", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "9:30\n\n15:45\n \n23:00" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 9, min: 30 }, "", { hour: 15, min: 45 }, @@ -196,21 +207,21 @@ describe("TimesOfDayList", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:05\n12:00\n15:30"); }); it("preserves order of times", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; act(() => { fireEvent.change(textarea, { target: { value: "23:59\n00:01\n12:30\n06:15" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ { hour: 23, min: 59 }, { hour: 0, min: 1 }, { hour: 12, min: 30 }, @@ -219,7 +230,7 @@ describe("TimesOfDayList", () => { }); it("handles complex regex edge cases", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render(); const textarea = getByRole("textbox"); @@ -228,7 +239,7 @@ describe("TimesOfDayList", () => { fireEvent.change(textarea, { target: { value: "1:2:3\n12:\n:30\n12:30:00\n12.30" } }); }); - expect(ref.current.state.testField).toEqual([ + expect(ref.current!.state.testField).toEqual([ "1:2:3", // Invalid - matches regex but has extra number "12:", // Invalid - no minutes ":30", // Invalid - no hours @@ -246,7 +257,7 @@ describe("TimesOfDayList", () => { const { getByTestId } = render(); - const textarea = getByTestId("times-input"); + const textarea = getByTestId("times-input") as HTMLTextAreaElement; expect(textarea.placeholder).toBe("Enter times"); expect(textarea.disabled).toBe(true); }); @@ -254,7 +265,7 @@ describe("TimesOfDayList", () => { it("has correct textarea attributes", () => { const { getByRole } = render(); - const textarea = getByRole("textbox"); + const textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.tagName).toBe("TEXTAREA"); expect(textarea.rows).toBe(5); expect(textarea.name).toBe("testField"); @@ -263,26 +274,26 @@ describe("TimesOfDayList", () => { it("displays validation feedback message", () => { const { container } = render(); - const feedback = container.querySelector(".invalid-feedback"); + const feedback = container.querySelector(".invalid-feedback") as HTMLElement; expect(feedback).toBeTruthy(); expect(feedback.textContent).toBe("Invalid Times of Day"); }); it("updates display when component state changes", () => { - let ref = React.createRef(); + const ref = React.createRef(); const { getByRole } = render( , ); - let textarea = getByRole("textbox"); + let textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("9:30"); // Update the component state and rerender act(() => { - ref.current.setState({ testField: [{ hour: 15, min: 45 }] }); + ref.current!.setState({ testField: [{ hour: 15, min: 45 }] }); }); - textarea = getByRole("textbox"); + textarea = getByRole("textbox") as HTMLTextAreaElement; expect(textarea.value).toBe("15:45"); }); }); diff --git a/tests/forms/forms.test.jsx b/tests/forms/forms.test.tsx similarity index 90% rename from tests/forms/forms.test.jsx rename to tests/forms/forms.test.tsx index 57bda1d5..2fd9812b 100644 --- a/tests/forms/forms.test.jsx +++ b/tests/forms/forms.test.tsx @@ -1,25 +1,27 @@ -import { render, fireEvent } from "@testing-library/react"; +import { OptionalDirectory } from "../../src/forms/OptionalDirectory"; +import { OptionalBoolean } from "../../src/forms/OptionalBoolean"; +import { LogDetailSelector } from "../../src/forms/LogDetailSelector"; +import { RequiredBoolean } from "../../src/forms/RequiredBoolean"; +import { isInvalidNumber, stateProperty, validateRequiredFields, valueToNumber } from "../../src/forms"; +import { listToMultilineString, multilineStringToList, StringList } from "../../src/forms/StringList"; +import { fireEvent, render } from "@testing-library/react"; import React from "react"; -import { vi } from "vitest"; -import PropTypes from "prop-types"; -import "@testing-library/jest-dom"; -import { validateRequiredFields, handleChange, stateProperty, valueToNumber, isInvalidNumber } from "../../src/forms"; -import { StringList, listToMultilineString, multilineStringToList } from "../../src/forms/StringList"; -import { OptionalFieldNoLabel } from "../../src/forms/OptionalFieldNoLabel"; import { RequiredField } from "../../src/forms/RequiredField"; import { OptionalField } from "../../src/forms/OptionalField"; -import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; +import { OptionalFieldNoLabel } from "../../src/forms/OptionalFieldNoLabel"; import { RequiredNumberField } from "../../src/forms/RequiredNumberField"; -import { RequiredDirectory } from "../../src/forms/RequiredDirectory"; import { OptionalNumberField } from "../../src/forms/OptionalNumberField"; -import { OptionalDirectory } from "../../src/forms/OptionalDirectory"; -import { OptionalBoolean } from "../../src/forms/OptionalBoolean"; -import { LogDetailSelector } from "../../src/forms/LogDetailSelector"; -import { RequiredBoolean } from "../../src/forms/RequiredBoolean"; +import { TimesOfDayList } from "../../src/forms/TimesOfDayList"; +import { RequiredDirectory } from "../../src/forms/RequiredDirectory"; +import { vi } from "vitest"; +import PropTypes from "prop-types"; +import { handleChange } from "../../src/forms"; // Mock component class to simulate React component behavior -class MockComponent extends React.Component { - state = {}; // Define state as class property +export class MockComponent extends React.Component { + state: { [key: string]: any } = {}; // Define state as class property + _isTestComponent: boolean; + handleChange: unknown; constructor(props) { super(props); @@ -46,7 +48,7 @@ class MockComponent extends React.Component { } // Test form component that uses all field types -function TestFormComponent({ component }) { +export function TestFormComponent({ component }) { return (

{RequiredField(component, "Required Text", "requiredText")} @@ -73,7 +75,7 @@ describe("Forms Utility Functions", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); describe("validateRequiredFields", () => { @@ -170,22 +172,26 @@ describe("StringList Component", () => { describe("multilineStringToList", () => { it("should convert multiline string to array", () => { - expect(multilineStringToList({ value: "line1\nline2\nline3" })).toEqual(["line1", "line2", "line3"]); + expect(multilineStringToList({ value: "line1\nline2\nline3" } as HTMLTextAreaElement)).toEqual([ + "line1", + "line2", + "line3", + ]); }); it("should return undefined for empty string", () => { - expect(multilineStringToList({ value: "" })).toBeUndefined(); + expect(multilineStringToList({ value: "" } as HTMLTextAreaElement)).toBeUndefined(); }); }); it("should render and handle changes", () => { - const component = new MockComponent(); + const component = new MockComponent({}); component.setTestState({ stringList: ["item1", "item2"] }); const { container } = render(
{StringList(component, "stringList")}
); - const textarea = container.querySelector('textarea[name="stringList"]'); - expect(textarea.value).toBe("item1\nitem2"); + const textarea = container.querySelector('textarea[name="stringList"]')!; + expect((textarea as HTMLTextAreaElement)!.value).toBe("item1\nitem2"); fireEvent.change(textarea, { target: { value: "new1\nnew2\nnew3" } }); expect(component.state.stringList).toEqual(["new1", "new2", "new3"]); @@ -196,7 +202,7 @@ describe("Form Field Components", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); describe("RequiredField", () => { @@ -204,10 +210,10 @@ describe("Form Field Components", () => { component.setTestState({ test: "" }); const { container } = render(
{RequiredField(component, "Test Label", "test")}
); - const input = container.querySelector('input[name="test"]'); + const input = container.querySelector('input[name="test"]')!; expect(input.classList.contains("is-invalid")).toBe(true); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(true); expect(label.textContent).toBe("Test Label"); }); @@ -216,7 +222,7 @@ describe("Form Field Components", () => { component.setTestState({ test: "value" }); const { container } = render(
{RequiredField(component, "Test Label", "test")}
); - const input = container.querySelector('input[name="test"]'); + const input = container.querySelector('input[name="test"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -225,10 +231,10 @@ describe("Form Field Components", () => { it("should render without validation styling", () => { const { container } = render(
{OptionalField(component, "Optional Label", "optional")}
); - const input = container.querySelector('input[name="optional"]'); + const input = container.querySelector('input[name="optional"]')!; expect(input.classList.contains("is-invalid")).toBe(false); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(false); }); }); @@ -247,7 +253,7 @@ describe("Form Field Components", () => { component.setTestState({ num: "abc" }); const { container } = render(
{RequiredNumberField(component, "Number", "num")}
); - const input = container.querySelector('input[name="num"]'); + const input = container.querySelector('input[name="num"]')!; expect(input.classList.contains("is-invalid")).toBe(true); }); @@ -255,7 +261,7 @@ describe("Form Field Components", () => { component.setTestState({ num: "123" }); const { container } = render(
{RequiredNumberField(component, "Number", "num")}
); - const input = container.querySelector('input[name="num"]'); + const input = container.querySelector('input[name="num"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -265,7 +271,7 @@ describe("Form Field Components", () => { component.setTestState({ optNum: "invalid" }); const { container } = render(
{OptionalNumberField(component, "Optional Number", "optNum")}
); - const input = container.querySelector('input[name="optNum"]'); + const input = container.querySelector('input[name="optNum"]')!; expect(input.classList.contains("is-invalid")).toBe(true); }); @@ -273,7 +279,7 @@ describe("Form Field Components", () => { component.setTestState({ optNum: "" }); const { container } = render(
{OptionalNumberField(component, "Optional Number", "optNum")}
); - const input = container.querySelector('input[name="optNum"]'); + const input = container.querySelector('input[name="optNum"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); }); @@ -283,14 +289,14 @@ describe("Form Field Components", () => { component.setTestState({ bool: true }); const { container } = render(
{RequiredBoolean(component, "Boolean Field", "bool")}
); - const checkbox = container.querySelector('input[type="checkbox"]'); + const checkbox = container.querySelector('input[type="checkbox"]')! as HTMLInputElement; expect(checkbox.checked).toBe(true); }); it("should handle checkbox changes", () => { const { container } = render(
{RequiredBoolean(component, "Boolean Field", "bool")}
); - const checkbox = container.querySelector('input[type="checkbox"]'); + const checkbox = container.querySelector('input[type="checkbox"]') as HTMLInputElement; fireEvent.click(checkbox); expect(component.state.bool).toBe(true); }); @@ -300,7 +306,7 @@ describe("Form Field Components", () => { it("should render select with three options", () => { const { container } = render(
{OptionalBoolean(component, "Optional Bool", "optBool", "Default")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; const options = select.querySelectorAll("option"); expect(options).toHaveLength(3); expect(options[0].textContent).toBe("Default"); @@ -311,7 +317,7 @@ describe("Form Field Components", () => { it("should handle value changes", () => { const { container } = render(
{OptionalBoolean(component, "Optional Bool", "optBool", "Default")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; fireEvent.change(select, { target: { value: "true" } }); expect(component.state.optBool).toBe(true); @@ -324,7 +330,7 @@ describe("Form Field Components", () => { it("should render with proper options", () => { const { container } = render(
{LogDetailSelector(component, "logLevel")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; const options = select.querySelectorAll("option"); expect(options.length).toBeGreaterThan(10); expect(options[0].textContent).toBe("(inherit from parent)"); @@ -333,7 +339,7 @@ describe("Form Field Components", () => { it("should handle numeric values", () => { const { container } = render(
{LogDetailSelector(component, "logLevel")}
); - const select = container.querySelector("select"); + const select = container.querySelector("select") as HTMLSelectElement; fireEvent.change(select, { target: { value: "5" } }); expect(component.state.logLevel).toBe(5); }); @@ -349,14 +355,14 @@ describe("Form Field Components", () => { }); const { container } = render(
{TimesOfDayList(component, "times")}
); - const textarea = container.querySelector('textarea[name="times"]'); - expect(textarea.value).toBe("9:30\n17:00"); + const textarea = container.querySelector('textarea[name="times"]') as HTMLTextAreaElement; + expect(textarea!.value).toBe("9:30\n17:00"); }); it("should parse time strings correctly", () => { const { container } = render(
{TimesOfDayList(component, "times")}
); - const textarea = container.querySelector('textarea[name="times"]'); + const textarea = container.querySelector('textarea[name="times"]') as HTMLTextAreaElement; fireEvent.change(textarea, { target: { value: "09:30\n17:00\ninvalid" } }); expect(component.state.times).toEqual([{ hour: 9, min: 30 }, { hour: 17, min: 0 }, "invalid"]); @@ -368,17 +374,17 @@ describe("Form Field Components", () => { component.setTestState({ dir: "" }); const { container } = render(
{RequiredDirectory(component, "Required Dir", "dir")}
); - const input = container.querySelector('input[name="dir"]'); + const input = container.querySelector('input[name="dir"]')!; expect(input.classList.contains("is-invalid")).toBe(true); - const label = container.querySelector("label"); + const label = container.querySelector("label")!; expect(label.classList.contains("required")).toBe(true); }); it("should render OptionalDirectory without validation", () => { const { container } = render(
{OptionalDirectory(component, "Optional Dir", "dir")}
); - const input = container.querySelector('input[name="dir"]'); + const input = container.querySelector('input[name="dir"]')!; expect(input.classList.contains("is-invalid")).toBe(false); }); @@ -392,7 +398,7 @@ describe("Form Field Components", () => { it("should show button when kopiaUI is available", () => { // Mock window.kopiaUI const mockSelectDirectory = vi.fn(); - globalThis.window.kopiaUI = { selectDirectory: mockSelectDirectory }; + (globalThis.window as unknown as { kopiaUI }).kopiaUI = { selectDirectory: mockSelectDirectory }; const { container } = render(
{RequiredDirectory(component, "Dir", "dir")}
); @@ -400,7 +406,7 @@ describe("Form Field Components", () => { expect(button).toBeTruthy(); // Clean up - delete globalThis.window.kopiaUI; + delete (globalThis.window as unknown as { kopiaUI }).kopiaUI; }); }); }); @@ -409,7 +415,7 @@ describe("Integrated Form Component", () => { let component; beforeEach(() => { - component = new MockComponent(); + component = new MockComponent({}); }); it("should render all field types together", () => { @@ -453,20 +459,20 @@ describe("Integrated Form Component", () => { expect(component.state.requiredBool).toBe(true); // Test select fields - const optionalBoolSelect = container.querySelector('select[name="optionalBool"]'); + const optionalBoolSelect = container.querySelector('select[name="optionalBool"]')!; fireEvent.change(optionalBoolSelect, { target: { value: "true" } }); expect(component.state.optionalBool).toBe(true); - const logLevelSelect = container.querySelector('select[name="logLevel"]'); + const logLevelSelect = container.querySelector('select[name="logLevel"]')!; fireEvent.change(logLevelSelect, { target: { value: "5" } }); expect(component.state.logLevel).toBe(5); // Test textarea fields - const stringListTextarea = container.querySelector('textarea[name="stringList"]'); + const stringListTextarea = container.querySelector('textarea[name="stringList"]')!; fireEvent.change(stringListTextarea, { target: { value: "item1\nitem2" } }); expect(component.state.stringList).toEqual(["item1", "item2"]); - const timesTextarea = container.querySelector('textarea[name="timesOfDay"]'); + const timesTextarea = container.querySelector('textarea[name="timesOfDay"]')!; fireEvent.change(timesTextarea, { target: { value: "09:30\n17:00" } }); expect(component.state.timesOfDay).toEqual([ { hour: 9, min: 30 }, diff --git a/tests/index.test.jsx b/tests/index.test.tsx similarity index 91% rename from tests/index.test.jsx rename to tests/index.test.tsx index f8a1165e..240b0147 100644 --- a/tests/index.test.jsx +++ b/tests/index.test.tsx @@ -68,10 +68,11 @@ describe("index.jsx", () => { test("handles missing root element by throwing error", async () => { // Mock getElementById to return null - document.getElementById.mockReturnValue(null); + (document.getElementById as jest.Mock).mockReturnValue(null); // Mock createRoot to throw an error when called with null - mockCreateRoot.mockImplementation((element) => { + mockCreateRoot.mockImplementation((...args) => { + const [element] = args as unknown as [HTMLElement | null]; if (!element) { throw new Error("Target container is not a DOM element."); } @@ -117,14 +118,14 @@ describe("index.jsx", () => { test("successfully loads CSS styles", async () => { // The CSS import is mocked, so we just verify the module loads without error - await expect(import("../src/index.jsx")).resolves.not.toThrow(); + await expect(import("../src/index.js")).resolves.not.toThrow(); }); test("calls createRoot and render in correct order", async () => { - const callOrder = []; + const callOrder: string[] = []; // Track call order - mockCreateRoot.mockImplementation((_element) => { + mockCreateRoot.mockImplementation((..._args) => { callOrder.push("createRoot"); return mockRoot; }); diff --git a/tests/pages/Policies.test.jsx b/tests/pages/Policies.test.tsx similarity index 98% rename from tests/pages/Policies.test.jsx rename to tests/pages/Policies.test.tsx index 556b9fec..b4969492 100644 --- a/tests/pages/Policies.test.jsx +++ b/tests/pages/Policies.test.tsx @@ -3,10 +3,10 @@ import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { BrowserRouter } from "react-router-dom"; -import { Policies, PoliciesInternal } from "../../src/pages/Policies"; -import { AppContext } from "../../src/contexts/AppContext"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Policies, PoliciesInternal } from "../../src/pages/Policies.js"; +import { AppContext } from "../../src/contexts/AppContext.js"; +import { PageSize, UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; @@ -25,7 +25,7 @@ const mockAppContextValue = { repoDescription: "Test Repository", }; -const mockUIPreferencesContext = { +const mockUIPreferencesContext: UIPreferences = { pageSize: 10, setPageSize: vi.fn(), theme: "light", diff --git a/tests/pages/Policy.test.jsx b/tests/pages/Policy.test.tsx similarity index 97% rename from tests/pages/Policy.test.jsx rename to tests/pages/Policy.test.tsx index 608a2494..84b898c2 100644 --- a/tests/pages/Policy.test.jsx +++ b/tests/pages/Policy.test.tsx @@ -5,9 +5,10 @@ import { render, screen } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { MemoryRouter } from "react-router-dom"; import "@testing-library/jest-dom"; -import { Policy } from "../../src/pages/Policy"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Policy } from "../../src/pages/Policy.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { mockNavigate, resetRouterMocks, updateRouterMocks } from "../testutils/react-router-mock.jsx"; +import { PolicyQueryParams } from "../../src/utils/policyutil.js"; // Mock React Router hooks using unified helper vi.mock("react-router-dom", async () => { @@ -17,7 +18,7 @@ vi.mock("react-router-dom", async () => { // Mock the PolicyEditor component to avoid complex dependencies vi.mock("../../src/components/policy-editor/PolicyEditor", () => ({ - PolicyEditor: React.forwardRef(function MockPolicyEditor(props, _ref) { + PolicyEditor: React.forwardRef(function MockPolicyEditor(props: PolicyQueryParams & { close }, _ref) { return (
PolicyEditor Mock
diff --git a/tests/pages/Preferences.test.jsx b/tests/pages/Preferences.test.tsx similarity index 92% rename from tests/pages/Preferences.test.jsx rename to tests/pages/Preferences.test.tsx index e93bf9c5..034771f5 100644 --- a/tests/pages/Preferences.test.jsx +++ b/tests/pages/Preferences.test.tsx @@ -34,7 +34,7 @@ describe("Select the light theme", () => { screen.getByRole("option", { name: "light" }), ); - expect(screen.getByRole("option", { name: "light" }).selected).toBe(true); + expect((screen.getByRole("option", { name: "light" }) as HTMLOptionElement).selected).toBe(true); }); }); diff --git a/tests/pages/Repository.test.jsx b/tests/pages/Repository.test.tsx similarity index 99% rename from tests/pages/Repository.test.jsx rename to tests/pages/Repository.test.tsx index 05dbfc79..c723ee88 100644 --- a/tests/pages/Repository.test.jsx +++ b/tests/pages/Repository.test.tsx @@ -299,7 +299,7 @@ describe("Repository component - initializing state", () => { if (callCount === 1) { return [200, { connected: false, initTaskID: "task-123" }]; } else { - return [200, { connected: true, description: "Connected!", ...connectedStatus }]; + return [200, { ...connectedStatus, connected: true, description: "Connected!" }]; } }); diff --git a/tests/pages/SnapshotCreate.test.jsx b/tests/pages/SnapshotCreate.test.tsx similarity index 97% rename from tests/pages/SnapshotCreate.test.jsx rename to tests/pages/SnapshotCreate.test.tsx index c6455bf0..e5dbac45 100644 --- a/tests/pages/SnapshotCreate.test.jsx +++ b/tests/pages/SnapshotCreate.test.tsx @@ -2,8 +2,8 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor, act } from "@testing-library/react"; -import { SnapshotCreate } from "../../src/pages/SnapshotCreate"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotCreate } from "../../src/pages/SnapshotCreate.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -31,7 +31,7 @@ vi.mock("../../src/components/GoBackButton", () => ({ // Mock PolicyEditor with a simple implementation that tracks ref calls vi.mock("../../src/components/policy-editor/PolicyEditor", () => ({ - PolicyEditor: React.forwardRef((props, ref) => { + PolicyEditor: React.forwardRef((props: { embedded; path }, ref) => { React.useImperativeHandle(ref, () => ({ getAndValidatePolicy: vi.fn(() => ({ somePolicy: "data" })), })); @@ -296,7 +296,7 @@ describe("SnapshotCreate component", () => { }); test("handles API errors gracefully", async () => { - const { errorAlert } = await import("../../src/utils/uiutil"); + const { errorAlert } = await import("../../src/utils/uiutil.js"); const mockErrorAlert = vi.mocked(errorAlert); axiosMock.onGet("/api/v1/sources").reply(200, { @@ -477,7 +477,7 @@ describe("SnapshotCreate component", () => { }); test("handles sources API failure on mount", async () => { - const { redirect } = await import("../../src/utils/uiutil"); + const { redirect } = await import("../../src/utils/uiutil.js"); const mockRedirect = vi.mocked(redirect); axiosMock.onGet("/api/v1/sources").reply(500, { diff --git a/tests/pages/SnapshotDirectory.test.jsx b/tests/pages/SnapshotDirectory.test.tsx similarity index 98% rename from tests/pages/SnapshotDirectory.test.jsx rename to tests/pages/SnapshotDirectory.test.tsx index cc13427f..36d3a4e5 100644 --- a/tests/pages/SnapshotDirectory.test.jsx +++ b/tests/pages/SnapshotDirectory.test.tsx @@ -2,10 +2,11 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { SnapshotDirectory } from "../../src/pages/SnapshotDirectory"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotDirectory } from "../../src/pages/SnapshotDirectory.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; +import "../../global.d.ts"; let axiosMock; @@ -33,7 +34,7 @@ vi.mock("../../src/components/DirectoryItems", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, diff --git a/tests/pages/SnapshotHistory.test.jsx b/tests/pages/SnapshotHistory.test.tsx similarity index 90% rename from tests/pages/SnapshotHistory.test.jsx rename to tests/pages/SnapshotHistory.test.tsx index c8dd9f3a..25fcb050 100644 --- a/tests/pages/SnapshotHistory.test.jsx +++ b/tests/pages/SnapshotHistory.test.tsx @@ -2,10 +2,10 @@ import React from "react"; import { render, screen, fireEvent, waitFor, act } from "@testing-library/react"; import "@testing-library/jest-dom"; import { describe, it, expect, vi, beforeEach } from "vitest"; -import { SnapshotHistory } from "../../src/pages/SnapshotHistory"; +import { SnapshotHistory } from "../../src/pages/SnapshotHistory.js"; import { BrowserRouter } from "react-router-dom"; import axios from "axios"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; import { mockNavigate, resetRouterMocks } from "../testutils/react-router-mock.jsx"; // Mock axios @@ -28,7 +28,7 @@ const renderWithProviders = (component) => { showIdenticalSnapshots: false, setShowIdenticalSnapshots: vi.fn(), bytesStringBase2: vi.fn((size) => `${size} bytes`), // Mock function for size formatting - }; + } as unknown as UIPreferences; return render( @@ -41,12 +41,12 @@ describe("SnapshotHistory", () => { beforeEach(() => { resetRouterMocks(); vi.clearAllMocks(); - axios.get.mockReset(); - axios.post.mockReset(); + (axios.get as jest.Mock).mockReset(); + (axios.post as jest.Mock).mockReset(); }); it("should render loading spinner initially", () => { - axios.get.mockImplementation(() => new Promise(() => {})); // Never resolves + (axios.get as jest.Mock).mockImplementation(() => new Promise(() => {})); // Never resolves renderWithProviders(); // Look for Bootstrap spinner div @@ -80,7 +80,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -100,7 +100,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -120,7 +120,7 @@ describe("SnapshotHistory", () => { }); it("should display error when fetch fails", async () => { - axios.get.mockRejectedValue(new Error("Network error")); + (axios.get as jest.Mock).mockRejectedValue(new Error("Network error")); // Mock console.error to avoid error output in tests const consoleSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -141,7 +141,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -156,7 +156,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -176,7 +176,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -199,7 +199,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -214,7 +214,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -272,7 +272,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(mockResponse); + (axios.get as jest.Mock).mockResolvedValue(mockResponse); renderWithProviders(); await waitFor(() => { @@ -341,7 +341,7 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); renderWithProviders(); await waitFor(() => { @@ -358,8 +358,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockResolvedValue({ data: { success: true } }); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockResolvedValue({ data: { success: true } }); renderWithProviders(); await waitFor(() => { @@ -382,7 +382,7 @@ describe("SnapshotHistory", () => { }); it("should display select all button when snapshots exist", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -394,7 +394,7 @@ describe("SnapshotHistory", () => { }); it("should show correct snapshot count", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -403,7 +403,7 @@ describe("SnapshotHistory", () => { }); it("should show table headers for snapshot data", async () => { - axios.get.mockResolvedValue(mockSnapshotsResponse); + (axios.get as jest.Mock).mockResolvedValue(mockSnapshotsResponse); renderWithProviders(); await waitFor(() => { @@ -426,8 +426,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockRejectedValue(new Error("Delete failed")); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockRejectedValue(new Error("Delete failed")); // Mock console.error to avoid test output noise const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); @@ -458,8 +458,8 @@ describe("SnapshotHistory", () => { }, }; - axios.get.mockResolvedValue(emptyResponse); - axios.post.mockResolvedValue({ data: { success: true } }); + (axios.get as jest.Mock).mockResolvedValue(emptyResponse); + (axios.post as jest.Mock).mockResolvedValue({ data: { success: true } }); renderWithProviders(); await waitFor(() => { diff --git a/tests/pages/SnapshotRestore.test.jsx b/tests/pages/SnapshotRestore.test.tsx similarity index 97% rename from tests/pages/SnapshotRestore.test.jsx rename to tests/pages/SnapshotRestore.test.tsx index c47f0713..0d174e12 100644 --- a/tests/pages/SnapshotRestore.test.jsx +++ b/tests/pages/SnapshotRestore.test.tsx @@ -1,9 +1,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; -import { SnapshotRestore } from "../../src/pages/SnapshotRestore"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { SnapshotRestore } from "../../src/pages/SnapshotRestore.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import { fireEvent } from "@testing-library/react"; import "@testing-library/jest-dom"; @@ -30,7 +30,7 @@ vi.mock("../../src/utils/uiutil", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -235,7 +235,7 @@ describe("SnapshotRestore component", () => { }); test("handles API error gracefully", async () => { - const { errorAlert } = await import("../../src/utils/uiutil"); + const { errorAlert } = await import("../../src/utils/uiutil.js"); const mockErrorAlert = vi.mocked(errorAlert); axiosMock.onPost("/api/v1/restore").reply(500, { diff --git a/tests/pages/Snapshots.test.jsx b/tests/pages/Snapshots.test.tsx similarity index 98% rename from tests/pages/Snapshots.test.jsx rename to tests/pages/Snapshots.test.tsx index 985c6801..afd4435c 100644 --- a/tests/pages/Snapshots.test.jsx +++ b/tests/pages/Snapshots.test.tsx @@ -2,9 +2,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { Snapshots } from "../../src/pages/Snapshots"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Snapshots } from "../../src/pages/Snapshots.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; let axiosMock; @@ -16,7 +16,7 @@ vi.mock("react-router-dom", async () => { }); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -174,7 +174,7 @@ describe("Snapshots component", () => { // The sync button contains an icon that has the onClick handler const syncButton = screen.getByTitle("Synchronize"); const syncIcon = syncButton.querySelector("svg"); - await userEvent.click(syncIcon); + await userEvent.click(syncIcon!); await waitFor(() => { expect(axiosMock.history.post).toHaveLength(1); diff --git a/tests/pages/Task.test.jsx b/tests/pages/Task.test.tsx similarity index 97% rename from tests/pages/Task.test.jsx rename to tests/pages/Task.test.tsx index 628c76b6..60f9a35c 100644 --- a/tests/pages/Task.test.jsx +++ b/tests/pages/Task.test.tsx @@ -1,9 +1,9 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; -import { Task } from "../../src/pages/Task"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Task } from "../../src/pages/Task.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; let axiosMock; @@ -26,7 +26,7 @@ vi.mock("../../src/components/Logs", () => ({ })); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, diff --git a/tests/pages/Tasks.test.jsx b/tests/pages/Tasks.test.tsx similarity index 97% rename from tests/pages/Tasks.test.jsx rename to tests/pages/Tasks.test.tsx index c6ade955..4907c5aa 100644 --- a/tests/pages/Tasks.test.jsx +++ b/tests/pages/Tasks.test.tsx @@ -2,12 +2,12 @@ import React from "react"; import { describe, test, expect, beforeEach, afterEach, vi } from "vitest"; import { render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; -import { Tasks } from "../../src/pages/Tasks"; -import { UIPreferencesContext } from "../../src/contexts/UIPreferencesContext"; -import { setupAPIMock } from "../testutils/api-mocks"; +import { Tasks } from "../../src/pages/Tasks.js"; +import { UIPreferences, UIPreferencesContext } from "../../src/contexts/UIPreferencesContext.js"; +import { setupAPIMock } from "../testutils/api-mocks.js"; import "@testing-library/jest-dom"; import { fireEvent } from "@testing-library/react"; -import { setupIntervalMocks, cleanupIntervalMocks, triggerIntervals } from "../testutils/interval-mocks"; +import { setupIntervalMocks, cleanupIntervalMocks, triggerIntervals } from "../testutils/interval-mocks.js"; let axiosMock; @@ -18,7 +18,7 @@ vi.mock("react-router-dom", async () => { }); // Minimal UIPreferences context value -const mockUIPreferences = { +const mockUIPreferences: UIPreferences = { pageSize: 10, theme: "light", bytesStringBase2: false, @@ -208,7 +208,7 @@ describe("Tasks component", () => { const snapshotOptions = screen.getAllByText("Snapshot"); // Click on the dropdown option (not the table content) const dropdownOption = snapshotOptions.find((el) => el.classList.contains("dropdown-item")); - await userEvent.click(dropdownOption); + await userEvent.click(dropdownOption!); // Should only show snapshot tasks expect(screen.getByText("Snapshot task")).toBeInTheDocument(); diff --git a/tests/testutils/api-mocks.js b/tests/testutils/api-mocks.ts similarity index 100% rename from tests/testutils/api-mocks.js rename to tests/testutils/api-mocks.ts diff --git a/tests/testutils/interval-mocks.js b/tests/testutils/interval-mocks.ts similarity index 86% rename from tests/testutils/interval-mocks.js rename to tests/testutils/interval-mocks.ts index 88d29b0a..1592302f 100644 --- a/tests/testutils/interval-mocks.js +++ b/tests/testutils/interval-mocks.ts @@ -1,8 +1,9 @@ import { vi } from "vitest"; +import "@testing-library/jest-dom"; let intervalSpy; let clearIntervalSpy; -let intervalCallbacks = []; +let intervalCallbacks: { id: number; callback; delay }[] = []; let intervalId = 0; /** @@ -16,7 +17,7 @@ export function setupIntervalMocks() { intervalSpy = vi.spyOn(window, "setInterval").mockImplementation((callback, delay) => { const id = ++intervalId; intervalCallbacks.push({ id, callback, delay }); - return id; + return id as unknown as ReturnType; }); clearIntervalSpy = vi.spyOn(window, "clearInterval").mockImplementation((id) => { @@ -52,7 +53,7 @@ export function getIntervalMockSpies() { export async function triggerIntervals() { const { act } = await import("@testing-library/react"); await act(async () => { - intervalCallbacks.forEach(({ callback }) => { + intervalCallbacks.forEach(({ callback }: { callback: () => void }) => { callback(); }); }); @@ -60,9 +61,9 @@ export async function triggerIntervals() { /** * Helper function to wait for component to load and then trigger intervals - * @param {RegExp|string} expectedText - Text to wait for before triggering intervals + * @param expectedText - Text to wait for before triggering intervals */ -export async function waitForLoadAndTriggerIntervals(expectedText) { +export async function waitForLoadAndTriggerIntervals(expectedText: RegExp | string) { const { waitFor, screen, act } = await import("@testing-library/react"); // Wait for the component to load first diff --git a/tests/testutils/react-router-mock.jsx b/tests/testutils/react-router-mock.tsx similarity index 83% rename from tests/testutils/react-router-mock.jsx rename to tests/testutils/react-router-mock.tsx index 49bcecc1..cf7cecd9 100644 --- a/tests/testutils/react-router-mock.jsx +++ b/tests/testutils/react-router-mock.tsx @@ -93,6 +93,17 @@ export function createRouterMock(options = {}) { searchParams = DEFAULT_STATE.searchParams, simple = false, components = {}, + }: { + location?: typeof DEFAULT_STATE.location; + params?: Record; + navigate?: typeof DEFAULT_STATE.navigate; + searchParams?: URLSearchParams | Record | string; + simple?: boolean; + components?: { + link?: boolean; + navLink?: boolean; + only?: boolean; + }; } = options; const { link: mockLink = true, navLink: mockNavLink = true, only: componentsOnly = false } = components; @@ -122,7 +133,7 @@ export function createRouterMock(options = {}) { // Component-only mock (minimal footprint) function createComponentOnlyMock({ mockLink, mockNavLink }) { return () => { - const mocks = {}; + const mocks: { Link?: typeof MockLink; NavLink?: typeof MockNavLink } = {}; if (mockLink) mocks.Link = MockLink; if (mockNavLink) mocks.NavLink = MockNavLink; return mocks; @@ -132,7 +143,14 @@ function createComponentOnlyMock({ mockLink, mockNavLink }) { // Simple mock (no actual implementation) function createSimpleMock({ navigate, mockLink, mockNavLink }) { return () => { - const mocks = { + const mocks: { + useNavigate: () => typeof navigate; + useLocation: () => ReturnType; + useParams: () => ReturnType; + useSearchParams: () => ReturnType; + Link?: typeof MockLink; + NavLink?: typeof MockNavLink; + } = { useNavigate: () => navigate, useLocation: () => mockUseLocation(), useParams: () => mockUseParams(), @@ -150,7 +168,14 @@ function createSimpleMock({ navigate, mockLink, mockNavLink }) { function createFullMock({ navigate, mockLink, mockNavLink }) { return async () => { const actual = await vi.importActual("react-router-dom"); - const mocks = { + const mocks: { + useNavigate: () => typeof navigate; + useLocation: () => ReturnType; + useParams: () => ReturnType; + useSearchParams: () => ReturnType; + Link?: typeof MockLink; + NavLink?: typeof MockNavLink; + } = { ...actual, useNavigate: () => navigate, useLocation: () => mockUseLocation(), @@ -185,7 +210,13 @@ export function resetRouterMocks() { * @param {Object} [state.params] - Params object to mock * @param {URLSearchParams|Object} [state.searchParams] - Search params to mock */ -export function updateRouterMocks(state = {}) { +export function updateRouterMocks( + state: { + location?: typeof DEFAULT_STATE.location; + params?: Record; + searchParams?: URLSearchParams | Record | string; + } = {}, +) { if (state.location) { mockUseLocation.mockReturnValue({ ...DEFAULT_STATE.location, ...state.location }); } diff --git a/tests/utils/deepstate.test.js b/tests/utils/deepstate.test.ts similarity index 100% rename from tests/utils/deepstate.test.js rename to tests/utils/deepstate.test.ts diff --git a/tests/utils/formatutils.test.js b/tests/utils/formatutils.test.ts similarity index 100% rename from tests/utils/formatutils.test.js rename to tests/utils/formatutils.test.ts diff --git a/tests/utils/policyutil.test.js b/tests/utils/policyutil.test.ts similarity index 96% rename from tests/utils/policyutil.test.js rename to tests/utils/policyutil.test.ts index d89b0a1e..a9b12233 100644 --- a/tests/utils/policyutil.test.js +++ b/tests/utils/policyutil.test.ts @@ -104,16 +104,16 @@ describe("sourceQueryStringParams", () => { expect(result).toContain("path=%2Fpath%20with%20spaces"); }); - it("handles undefined or null values", () => { + it("handles undefined values", () => { const source = { userName: undefined, host: "examplehost", - path: null, + path: undefined, }; const result = sourceQueryStringParams(source); - expect(result).toContain("userName=undefined"); + expect(result).toContain("userName="); expect(result).toContain("host=examplehost"); - expect(result).toContain("path=null"); + expect(result).toContain("path="); }); it("handles empty values", () => { @@ -198,7 +198,7 @@ describe("policyEditorURL", () => { it("handles empty source", () => { const source = {}; const url = policyEditorURL(source); - expect(url).toBe("/policies/edit?userName=undefined&host=undefined&path=undefined"); + expect(url).toBe("/policies/edit?userName=&host=&path="); }); it("generates URL for global policy", () => { diff --git a/tests/utils/uiutil-browser.test.js b/tests/utils/uiutil-browser.test.ts similarity index 88% rename from tests/utils/uiutil-browser.test.js rename to tests/utils/uiutil-browser.test.ts index 9d5c7b5f..3d88d5a2 100644 --- a/tests/utils/uiutil-browser.test.js +++ b/tests/utils/uiutil-browser.test.ts @@ -6,16 +6,21 @@ describe("redirect", () => { beforeAll(() => { // Mock window.location.replace - delete window.location; - window.location = { replace: vi.fn() }; + Object.defineProperty(window, "location", { + configurable: true, + value: { replace: vi.fn() }, + }); }); afterAll(() => { - window.location = originalLocation; + Object.defineProperty(window, "location", { + configurable: true, + value: originalLocation, + }); }); beforeEach(() => { - window.location.replace.mockClear(); + (window.location.replace as jest.Mock).mockClear(); }); it("redirects to /repo when error code is NOT_CONNECTED", () => { @@ -69,7 +74,7 @@ describe("errorAlert", () => { }); beforeEach(() => { - window.alert.mockClear(); + (window.alert as jest.Mock).mockClear(); }); it("shows error message from response data", () => { diff --git a/tests/utils/uiutil-components.test.jsx b/tests/utils/uiutil-components.test.tsx similarity index 88% rename from tests/utils/uiutil-components.test.jsx rename to tests/utils/uiutil-components.test.tsx index e3b67729..6c35e978 100644 --- a/tests/utils/uiutil-components.test.jsx +++ b/tests/utils/uiutil-components.test.tsx @@ -4,19 +4,29 @@ import "@testing-library/jest-dom"; import { sizeWithFailures } from "../../src/utils/uiutil"; import { taskStatusSymbol } from "../../src/utils/taskutil"; +const sizeWithFailuresNoElement = new Error( + "sizeWithFailures did not return a React element, this is not expected behavior", +); + describe("sizeWithFailures", () => { it("returns empty string for undefined size", () => { - expect(sizeWithFailures(undefined)).toBe(""); + expect(sizeWithFailures(undefined, undefined, false)).toBe(""); }); it("returns simple size display without errors", () => { const result = sizeWithFailures(1024, null, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.props.children).toBe("1 KB"); }); it("returns simple size display when no failures", () => { const summ = { errors: [], numFailed: 0 }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.props.children).toBe("1 KB"); }); @@ -26,6 +36,9 @@ describe("sizeWithFailures", () => { numFailed: 1, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } // Should be a span containing size, nbsp, and error icon expect(result.type).toBe("span"); @@ -43,6 +56,9 @@ describe("sizeWithFailures", () => { numFailed: 2, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } expect(result.type).toBe("span"); // Check that error icon has the correct title format @@ -58,6 +74,9 @@ describe("sizeWithFailures", () => { numFailed: 1, }; const result = sizeWithFailures(1024, summ, false); + if (typeof result === "string") { + throw sizeWithFailuresNoElement; + } const errorIcon = result.props.children[2]; // Third element is the icon expect(errorIcon.props.title).toContain("Error: "); diff --git a/tests/utils/uiutil.test.js b/tests/utils/uiutil.test.ts similarity index 100% rename from tests/utils/uiutil.test.js rename to tests/utils/uiutil.test.ts diff --git a/tsconfig.json b/tsconfig.json index bd9c392f..61bec004 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,7 +4,7 @@ "lib": ["dom", "dom.iterable", "esnext"], "allowJs": false, "skipLibCheck": true, - "esModuleInterop": false, + "esModuleInterop": true, "allowSyntheticDefaultImports": true, "strict": true, "forceConsistentCasingInFileNames": true, @@ -14,9 +14,10 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, - "jsx": "react-jsx", + "jsx": "react", "baseUrl": "./", "paths": {} }, - "include": ["src"] + "include": ["src/**/*", "global.d.ts", "test/global.d.ts"], + "exclude": ["node_modules", "build", "dist"] } diff --git a/vite.config.js b/vite.config.ts similarity index 84% rename from vite.config.js rename to vite.config.ts index 110d5c82..6072b893 100644 --- a/vite.config.js +++ b/vite.config.ts @@ -20,12 +20,12 @@ export default defineConfig(() => { server: { port: 3000, host: "localhost", - https: false, + https: undefined, strictPort: true, - open: true, + open: process.env.VITE_KOPIA_ENDPOINT ? false : true, proxy: { "/api": { - target: "http://localhost:51515", + target: process.env.VITE_KOPIA_ENDPOINT, changeOrigin: true, secure: false, },