diff --git a/frontend/package-lock.json b/frontend/package-lock.json
index 559fe98e1..395beb025 100644
--- a/frontend/package-lock.json
+++ b/frontend/package-lock.json
@@ -55,6 +55,7 @@
"husky": "^9.0.11",
"jsdom": "^26.0.0",
"lodash": "^4.17.21",
+ "msw": "^2.7.0",
"postcss-preset-mantine": "^1.14.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.5",
@@ -1912,6 +1913,63 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
+ "node_modules/@bundled-es-modules/cookie": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/cookie/-/cookie-2.0.1.tgz",
+ "integrity": "sha512-8o+5fRPLNbjbdGRRmJj3h6Hh1AQJf2dk3qQ/5ZFb+PXkRNiSoMGGUKlsgLfrxneb72axVJyIYji64E2+nNfYyw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "cookie": "^0.7.2"
+ }
+ },
+ "node_modules/@bundled-es-modules/cookie/node_modules/cookie": {
+ "version": "0.7.2",
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/@bundled-es-modules/statuses": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/statuses/-/statuses-1.0.1.tgz",
+ "integrity": "sha512-yn7BklA5acgcBr+7w064fGV+SGIFySjCKpqjcWgBAIfrAkY+4GQTJJHQMeT3V/sgz23VTEVV8TtOmkvJAhFVfg==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "statuses": "^2.0.1"
+ }
+ },
+ "node_modules/@bundled-es-modules/tough-cookie": {
+ "version": "0.1.6",
+ "resolved": "https://registry.npmjs.org/@bundled-es-modules/tough-cookie/-/tough-cookie-0.1.6.tgz",
+ "integrity": "sha512-dvMHbL464C0zI+Yqxbz6kZ5TOEp7GLW+pry/RWndAR8MJQAXZ2rPmIs8tziTZjeIyhSNZgZbCePtfSbdWqStJw==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "@types/tough-cookie": "^4.0.5",
+ "tough-cookie": "^4.1.4"
+ }
+ },
+ "node_modules/@bundled-es-modules/tough-cookie/node_modules/tough-cookie": {
+ "version": "4.1.4",
+ "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.1.4.tgz",
+ "integrity": "sha512-Loo5UUvLD9ScZ6jh8beX1T6sO1w2/MpCRpEP7V280GKMVUQ0Jzar2U3UJPsrdbziLEMMhu3Ujnq//rhiFuIeag==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "psl": "^1.1.33",
+ "punycode": "^2.1.1",
+ "universalify": "^0.2.0",
+ "url-parse": "^1.5.3"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/@canvas/image-data": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@canvas/image-data/-/image-data-1.0.0.tgz",
@@ -2675,6 +2733,80 @@
"integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==",
"dev": true
},
+ "node_modules/@inquirer/confirm": {
+ "version": "5.1.3",
+ "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.3.tgz",
+ "integrity": "sha512-fuF9laMmHoOgWapF9h9hv6opA5WvmGFHsTYGCmuFxcghIhEhb3dN0CdQR4BUMqa2H506NCj8cGX4jwMsE4t6dA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/core": "^10.1.4",
+ "@inquirer/type": "^3.0.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/core": {
+ "version": "10.1.4",
+ "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz",
+ "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@inquirer/figures": "^1.0.9",
+ "@inquirer/type": "^3.0.2",
+ "ansi-escapes": "^4.3.2",
+ "cli-width": "^4.1.0",
+ "mute-stream": "^2.0.0",
+ "signal-exit": "^4.1.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^6.2.0",
+ "yoctocolors-cjs": "^2.1.2"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/core/node_modules/signal-exit": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
+ "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=14"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "node_modules/@inquirer/figures": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz",
+ "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@inquirer/type": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz",
+ "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@types/node": ">=18"
+ }
+ },
"node_modules/@istanbuljs/schema": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz",
@@ -2888,6 +3020,24 @@
"react": "^18.x || ^19.x"
}
},
+ "node_modules/@mswjs/interceptors": {
+ "version": "0.37.5",
+ "resolved": "https://registry.npmjs.org/@mswjs/interceptors/-/interceptors-0.37.5.tgz",
+ "integrity": "sha512-AAwRb5vXFcY4L+FvZ7LZusDuZ0vEe0Zm8ohn1FM6/X7A3bj4mqmkAcGRWuvC2JwSygNwHAAmMnAI73vPHeqsHA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/logger": "^0.3.0",
+ "@open-draft/until": "^2.0.0",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "strict-event-emitter": "^0.5.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@@ -2923,6 +3073,31 @@
"node": ">= 8"
}
},
+ "node_modules/@open-draft/deferred-promise": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/deferred-promise/-/deferred-promise-2.2.0.tgz",
+ "integrity": "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@open-draft/logger": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/logger/-/logger-0.3.0.tgz",
+ "integrity": "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.0"
+ }
+ },
+ "node_modules/@open-draft/until": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/@open-draft/until/-/until-2.1.0.tgz",
+ "integrity": "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@polka/url": {
"version": "1.0.0-next.25",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.25.tgz",
@@ -3729,6 +3904,20 @@
"integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==",
"dev": true
},
+ "node_modules/@types/statuses": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz",
+ "integrity": "sha512-jmIUGWrAiwu3dZpxntxieC+1n/5c3mjrImkmOSQ2NC5uP6cYO4aAZDdSmRcI5C1oiTmqlZGHC+/NmJrKogbP5A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/tough-cookie": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz",
+ "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/trusted-types": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz",
@@ -5330,6 +5519,49 @@
"node": ">=8"
}
},
+ "node_modules/cli-width": {
+ "version": "4.1.0",
+ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz",
+ "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/cliui/node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/clsx": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz",
@@ -5961,6 +6193,13 @@
"integrity": "sha512-oJRPo82XEqtQAobHpJIR3zW5YO3sSRRkPz2an4yxi1UvqhsGm54vR/wzTFV74a3soDOJ8CKW7ajOOX5ESzddwg==",
"dev": true
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/end-of-stream": {
"version": "1.4.4",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz",
@@ -6777,6 +7016,16 @@
"node": ">=6.9.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
"node_modules/get-func-name": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz",
@@ -6957,6 +7206,16 @@
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==",
"dev": true
},
+ "node_modules/graphql": {
+ "version": "16.10.0",
+ "resolved": "https://registry.npmjs.org/graphql/-/graphql-16.10.0.tgz",
+ "integrity": "sha512-AjqGKbDGUFRKIRCP9tCKiIGHyriz2oHEbPIbEtcSLSs4YjReZOIPQQWek4+6hjw62H9QShXHyaGivGiYVLeYFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0"
+ }
+ },
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@@ -7038,6 +7297,13 @@
"node": ">= 0.4"
}
},
+ "node_modules/headers-polyfill": {
+ "version": "4.0.3",
+ "resolved": "https://registry.npmjs.org/headers-polyfill/-/headers-polyfill-4.0.3.tgz",
+ "integrity": "sha512-IScLbePpkvO846sIwOtOTDjutRMWdXdJmXdMvk6gCBHxFO8d+QKOQedyZSxFTTFYRSmlgSTDtXqqq4pcenBXLQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/html-encoding-sniffer": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz",
@@ -7384,6 +7650,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -7415,6 +7691,13 @@
"url": "https://github.com/sponsors/ljharb"
}
},
+ "node_modules/is-node-process": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/is-node-process/-/is-node-process-1.2.0.tgz",
+ "integrity": "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/is-number": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
@@ -8351,6 +8634,74 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
+ "node_modules/msw": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/msw/-/msw-2.7.0.tgz",
+ "integrity": "sha512-BIodwZ19RWfCbYTxWTUfTXc+sg4OwjCAgxU1ZsgmggX/7S3LdUifsbUPJs61j0rWb19CZRGY5if77duhc0uXzw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "dependencies": {
+ "@bundled-es-modules/cookie": "^2.0.1",
+ "@bundled-es-modules/statuses": "^1.0.1",
+ "@bundled-es-modules/tough-cookie": "^0.1.6",
+ "@inquirer/confirm": "^5.0.0",
+ "@mswjs/interceptors": "^0.37.0",
+ "@open-draft/deferred-promise": "^2.2.0",
+ "@open-draft/until": "^2.1.0",
+ "@types/cookie": "^0.6.0",
+ "@types/statuses": "^2.0.4",
+ "graphql": "^16.8.1",
+ "headers-polyfill": "^4.0.2",
+ "is-node-process": "^1.2.0",
+ "outvariant": "^1.4.3",
+ "path-to-regexp": "^6.3.0",
+ "picocolors": "^1.1.1",
+ "strict-event-emitter": "^0.5.1",
+ "type-fest": "^4.26.1",
+ "yargs": "^17.7.2"
+ },
+ "bin": {
+ "msw": "cli/index.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mswjs"
+ },
+ "peerDependencies": {
+ "typescript": ">= 4.8.x"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/msw/node_modules/type-fest": {
+ "version": "4.32.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.32.0.tgz",
+ "integrity": "sha512-rfgpoi08xagF3JSdtJlCwMq9DGNDE0IMh3Mkpc1wUypg9vPi786AiqeBBKcqvIkq42azsBM85N490fyZjeUftw==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/mute-stream": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz",
+ "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^18.17.0 || >=20.5.0"
+ }
+ },
"node_modules/nanoid": {
"version": "3.3.8",
"resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
@@ -8535,6 +8886,13 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/outvariant": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/outvariant/-/outvariant-1.4.3.tgz",
+ "integrity": "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/p-limit": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz",
@@ -8623,6 +8981,13 @@
"integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==",
"dev": true
},
+ "node_modules/path-to-regexp": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz",
+ "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/path-type": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
@@ -8648,9 +9013,9 @@
}
},
"node_modules/picocolors": {
- "version": "1.1.0",
- "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
- "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
"dev": true,
"license": "ISC"
},
@@ -9017,6 +9382,19 @@
"resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
"integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="
},
+ "node_modules/psl": {
+ "version": "1.15.0",
+ "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz",
+ "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "punycode": "^2.3.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/lupomontero"
+ }
+ },
"node_modules/pump": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz",
@@ -9037,6 +9415,13 @@
"node": ">=6"
}
},
+ "node_modules/querystringify": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz",
+ "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/queue-microtask": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
@@ -9469,6 +9854,16 @@
"jsesc": "bin/jsesc"
}
},
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
"node_modules/require-from-string": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
@@ -9479,6 +9874,13 @@
"node": ">=0.10.0"
}
},
+ "node_modules/requires-port": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
+ "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/resolve": {
"version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -10078,6 +10480,16 @@
"integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==",
"dev": true
},
+ "node_modules/statuses": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",
+ "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/std-env": {
"version": "3.7.0",
"resolved": "https://registry.npmjs.org/std-env/-/std-env-3.7.0.tgz",
@@ -10099,6 +10511,13 @@
"bare-events": "^2.2.0"
}
},
+ "node_modules/strict-event-emitter": {
+ "version": "0.5.1",
+ "resolved": "https://registry.npmjs.org/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz",
+ "integrity": "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/string_decoder": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
@@ -10109,6 +10528,21 @@
"safe-buffer": "~5.2.0"
}
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/string.prototype.matchall": {
"version": "4.0.11",
"resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.11.tgz",
@@ -10833,6 +11267,16 @@
"node": ">=8"
}
},
+ "node_modules/universalify": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.2.0.tgz",
+ "integrity": "sha512-CJ1QgKmNg3CwvAv/kOFmtnEN05f0D/cn9QntgNOQlQF9dgvVTHj3t+8JPdjqawCHk7V/KA+fbUqzZ9XWhcqPUg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4.0.0"
+ }
+ },
"node_modules/upath": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz",
@@ -10883,6 +11327,17 @@
"punycode": "^2.1.0"
}
},
+ "node_modules/url-parse": {
+ "version": "1.5.10",
+ "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz",
+ "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "querystringify": "^2.1.1",
+ "requires-port": "^1.0.0"
+ }
+ },
"node_modules/use-callback-ref": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz",
@@ -12029,6 +12484,21 @@
"workbox-core": "7.1.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz",
+ "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/wrappy": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
@@ -12079,6 +12549,16 @@
"node": ">=0.4.0"
}
},
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
"node_modules/yallist": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz",
@@ -12097,6 +12577,35 @@
"node": ">= 14"
}
},
+ "node_modules/yargs": {
+ "version": "17.7.2",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",
+ "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -12108,6 +12617,19 @@
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
+ },
+ "node_modules/yoctocolors-cjs": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz",
+ "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
}
}
}
diff --git a/frontend/package.json b/frontend/package.json
index 78b9f8ad4..80a945f21 100644
--- a/frontend/package.json
+++ b/frontend/package.json
@@ -59,6 +59,7 @@
"husky": "^9.0.11",
"jsdom": "^26.0.0",
"lodash": "^4.17.21",
+ "msw": "^2.7.0",
"postcss-preset-mantine": "^1.14.4",
"postcss-simple-vars": "^7.0.1",
"prettier": "^3.2.5",
diff --git a/frontend/src/App/app.test.tsx b/frontend/src/App/app.test.tsx
index db9895305..06aafaf86 100644
--- a/frontend/src/App/app.test.tsx
+++ b/frontend/src/App/app.test.tsx
@@ -1,9 +1,18 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
import { describe, it } from "vitest";
import { render } from "@/tests";
+import server from "@/tests/mocks/node";
import App from ".";
describe("App", () => {
it("should render without crash", () => {
+ server.use(
+ http.get("/api/system/searches", () => {
+ return HttpResponse.json({});
+ }),
+ );
+
render();
});
});
diff --git a/frontend/src/pages/Blacklist/Movies/index.test.tsx b/frontend/src/pages/Blacklist/Movies/index.test.tsx
new file mode 100644
index 000000000..a2fc491e4
--- /dev/null
+++ b/frontend/src/pages/Blacklist/Movies/index.test.tsx
@@ -0,0 +1,72 @@
+/* eslint-disable camelcase */
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render, screen, waitFor } from "@/tests";
+import server from "@/tests/mocks/node";
+import BlacklistMoviesView from ".";
+
+describe("Blacklist Movies", () => {
+ it("should render with blacklisted movies", async () => {
+ server.use(
+ http.get("/api/system/settings", () => {
+ return HttpResponse.json({
+ general: {
+ theme: "auto",
+ },
+ });
+ }),
+ );
+
+ server.use(
+ http.get("/api/movies/blacklist", () => {
+ // TODO: Replace with Factory
+ return HttpResponse.json({
+ data: [
+ {
+ title: "Batman vs Teenage Mutant Ninja Turtles",
+ radarrId: 50,
+ provider: "yifysubtitles",
+ subs_id:
+ "https://yifysubtitles.ch/subtitles/batman-vs-teenage-mutant-ninja-turtles-2019-english-yify-19252",
+ language: {
+ name: "English",
+ code2: "en",
+ code3: "eng",
+ forced: false,
+ hi: false,
+ },
+ timestamp: "28 seconds ago",
+ parsed_timestamp: "01/23/25 05:39:36",
+ },
+ ],
+ });
+ }),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText("yifysubtitles")).toBeInTheDocument();
+ });
+ });
+
+ it("should render without blacklisted movies", async () => {
+ server.use(
+ http.get("/api/movies/blacklist", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("No blacklisted movies subtitles"),
+ ).toBeInTheDocument();
+ });
+
+ server.resetHandlers();
+ });
+});
diff --git a/frontend/src/pages/Blacklist/Series/index.test.tsx b/frontend/src/pages/Blacklist/Series/index.test.tsx
new file mode 100644
index 000000000..4ea012fb6
--- /dev/null
+++ b/frontend/src/pages/Blacklist/Series/index.test.tsx
@@ -0,0 +1,62 @@
+/* eslint-disable camelcase */
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render, screen, waitFor } from "@/tests";
+import server from "@/tests/mocks/node";
+import BlacklistSeriesView from ".";
+
+describe("Blacklist Series", () => {
+ it("should render without blacklisted series", async () => {
+ server.use(
+ http.get("/api/episodes/blacklist", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(
+ screen.getByText("No blacklisted series subtitles"),
+ ).toBeInTheDocument();
+ });
+ });
+
+ it("should render with blacklisted series", async () => {
+ server.use(
+ http.get("/api/episodes/blacklist", () => {
+ // TODO: Replace with Factory
+ return HttpResponse.json({
+ data: [
+ {
+ seriesTitle: "Dragon Ball DAIMA",
+ episode_number: "1x14",
+ episodeTitle: "Taboo",
+ sonarrSeriesId: 56,
+ provider: "animetosho",
+ subs_id:
+ "https://animetosho.org/storage/attach/0022fd50/2293072.xz",
+ language: {
+ name: "English",
+ code2: "en",
+ code3: "eng",
+ forced: false,
+ hi: false,
+ },
+ timestamp: "now",
+ parsed_timestamp: "01/24/25 01:38:03",
+ },
+ ],
+ });
+ }),
+ );
+
+ render();
+
+ await waitFor(() => {
+ expect(screen.getByText("animetosho")).toBeInTheDocument();
+ });
+ });
+});
diff --git a/frontend/src/pages/Blacklist/blacklist.test.tsx b/frontend/src/pages/Blacklist/blacklist.test.tsx
deleted file mode 100644
index 4360c473c..000000000
--- a/frontend/src/pages/Blacklist/blacklist.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { renderTest, RenderTestCase } from "@/tests/render";
-import BlacklistMoviesView from "./Movies";
-import BlacklistSeriesView from "./Series";
-
-const cases: RenderTestCase[] = [
- {
- name: "movie page",
- ui: BlacklistMoviesView,
- },
- {
- name: "series page",
- ui: BlacklistSeriesView,
- },
-];
-
-renderTest("Blacklist", cases);
diff --git a/frontend/src/pages/History/Movies/index.test.tsx b/frontend/src/pages/History/Movies/index.test.tsx
new file mode 100644
index 000000000..f31c093bc
--- /dev/null
+++ b/frontend/src/pages/History/Movies/index.test.tsx
@@ -0,0 +1,33 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import MoviesHistoryView from ".";
+
+describe("History Movies", () => {
+ it("should render with movies", async () => {
+ server.use(
+ http.get("/api/movies/history", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ server.use(
+ http.get("/api/providers", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ server.use(
+ http.get("/api/system/languages", () => {
+ return HttpResponse.json({});
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/History/Series/index.test.tsx b/frontend/src/pages/History/Series/index.test.tsx
new file mode 100644
index 000000000..b60cb5e9e
--- /dev/null
+++ b/frontend/src/pages/History/Series/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SeriesHistoryView from ".";
+
+describe("History Series", () => {
+ it("should render with series", async () => {
+ server.use(
+ http.get("/api/episodes/history", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/History/Statistics/HistoryStats.test.tsx b/frontend/src/pages/History/Statistics/HistoryStats.test.tsx
new file mode 100644
index 000000000..66fc88403
--- /dev/null
+++ b/frontend/src/pages/History/Statistics/HistoryStats.test.tsx
@@ -0,0 +1,37 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import HistoryStats from "./HistoryStats";
+
+describe("History Stats", () => {
+ it("should render without stats", async () => {
+ server.use(
+ http.get("/api/providers", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ server.use(
+ http.get("/api/system/languages", () => {
+ return HttpResponse.json({});
+ }),
+ );
+ server.use(
+ http.get("/api/history/stats", () => {
+ return HttpResponse.json({
+ series: [],
+ });
+ }),
+ );
+
+ server.use(
+ http.get("/api/system/providers", () => {
+ return HttpResponse.json({});
+ }),
+ );
+
+ render();
+ });
+});
diff --git a/frontend/src/pages/History/history.test.tsx b/frontend/src/pages/History/history.test.tsx
deleted file mode 100644
index 277a268fb..000000000
--- a/frontend/src/pages/History/history.test.tsx
+++ /dev/null
@@ -1,21 +0,0 @@
-import { renderTest, RenderTestCase } from "@/tests/render";
-import HistoryStats from "./Statistics/HistoryStats";
-import MoviesHistoryView from "./Movies";
-import SeriesHistoryView from "./Series";
-
-const cases: RenderTestCase[] = [
- {
- name: "movie page",
- ui: MoviesHistoryView,
- },
- {
- name: "series page",
- ui: SeriesHistoryView,
- },
- {
- name: "statistics page",
- ui: HistoryStats,
- },
-];
-
-renderTest("History", cases);
diff --git a/frontend/src/pages/Movies/movies.test.tsx b/frontend/src/pages/Movies/movies.test.tsx
index c4ac8133a..673483bfa 100644
--- a/frontend/src/pages/Movies/movies.test.tsx
+++ b/frontend/src/pages/Movies/movies.test.tsx
@@ -1,16 +1,46 @@
-import { describe } from "vitest";
-import { render } from "@/tests";
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { beforeEach, describe, it } from "vitest";
+import { render, screen } from "@/tests";
+import server from "@/tests/mocks/node";
import MovieMassEditor from "./Editor";
import MovieView from ".";
describe("Movies page", () => {
+ beforeEach(() => {
+ server.use(
+ http.get("/api/movies", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ });
+
it("should render", () => {
render();
});
});
describe("Movies editor page", () => {
+ beforeEach(() => {
+ server.use(
+ http.get("/api/movies", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ server.use(
+ http.get("/api/system/languages/profiles", () => {
+ return HttpResponse.json([]);
+ }),
+ );
+ });
+
it("should render", () => {
render();
+
+ expect(screen.getByText("Actions")).toBeInTheDocument();
});
});
diff --git a/frontend/src/pages/Series/series.test.tsx b/frontend/src/pages/Series/series.test.tsx
index b8fd9fad5..decb7fd7c 100644
--- a/frontend/src/pages/Series/series.test.tsx
+++ b/frontend/src/pages/Series/series.test.tsx
@@ -1,15 +1,43 @@
-import { describe } from "vitest";
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { beforeEach, describe, it } from "vitest";
import { render } from "@/tests";
+import server from "@/tests/mocks/node";
import SeriesMassEditor from "./Editor";
import SeriesView from ".";
describe("Series page", () => {
+ beforeEach(() => {
+ server.use(
+ http.get("/api/series", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ });
+
it("should render", () => {
render();
});
});
describe("Series editor page", () => {
+ beforeEach(() => {
+ server.use(
+ http.get("/api/series", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ server.use(
+ http.get("/api/system/languages/profiles", () => {
+ return HttpResponse.json([]);
+ }),
+ );
+ });
+
it("should render", () => {
render();
});
diff --git a/frontend/src/pages/Settings/components/Section.test.tsx b/frontend/src/pages/Settings/components/Section.test.tsx
index 535bd8be2..90322488b 100644
--- a/frontend/src/pages/Settings/components/Section.test.tsx
+++ b/frontend/src/pages/Settings/components/Section.test.tsx
@@ -5,6 +5,7 @@ import { Section } from "./Section";
describe("Settings section", () => {
const header = "Section Header";
+
it("should show header", () => {
render();
diff --git a/frontend/src/pages/Settings/settings.test.tsx b/frontend/src/pages/Settings/settings.test.tsx
index 71aa74158..6dfb6e195 100644
--- a/frontend/src/pages/Settings/settings.test.tsx
+++ b/frontend/src/pages/Settings/settings.test.tsx
@@ -1,11 +1,11 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import server from "@/tests/mocks/node";
import { renderTest, RenderTestCase } from "@/tests/render";
import SettingsGeneralView from "./General";
import SettingsLanguagesView from "./Languages";
-import SettingsNotificationsView from "./Notifications";
import SettingsProvidersView from "./Providers";
-import SettingsRadarrView from "./Radarr";
import SettingsSchedulerView from "./Scheduler";
-import SettingsSonarrView from "./Sonarr";
import SettingsSubtitlesView from "./Subtitles";
import SettingsUIView from "./UI";
@@ -17,27 +17,32 @@ const cases: RenderTestCase[] = [
{
name: "languages page",
ui: SettingsLanguagesView,
- },
- {
- name: "notifications page",
- ui: SettingsNotificationsView,
- },
+ setupEach: () => {
+ server.use(
+ http.get("/api/system/languages", () => {
+ return HttpResponse.json({});
+ }),
+ );
+ server.use(
+ http.get("/api/system/languages/profiles", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+ },
+ },
+ // TODO: Test Notifications Page
{
name: "providers page",
ui: SettingsProvidersView,
},
- {
- name: "radarr page",
- ui: SettingsRadarrView,
- },
+ // TODO: Test Radarr Page
{
name: "scheduler page",
ui: SettingsSchedulerView,
},
- {
- name: "sonarr page",
- ui: SettingsSonarrView,
- },
+ // TODO: Test Sonarr Page
{
name: "subtitles page",
ui: SettingsSubtitlesView,
diff --git a/frontend/src/pages/System/Announcements/index.test.tsx b/frontend/src/pages/System/Announcements/index.test.tsx
new file mode 100644
index 000000000..cc7d18e75
--- /dev/null
+++ b/frontend/src/pages/System/Announcements/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemAnnouncementsView from ".";
+
+describe("System Announcements", () => {
+ it("should render with announcements", async () => {
+ server.use(
+ http.get("/api/system/announcements", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Backups/index.test.tsx b/frontend/src/pages/System/Backups/index.test.tsx
new file mode 100644
index 000000000..2b82177c6
--- /dev/null
+++ b/frontend/src/pages/System/Backups/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemBackupsView from ".";
+
+describe("System Backups", () => {
+ it("should render with backups", async () => {
+ server.use(
+ http.get("/api/system/backups", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Logs/index.test.tsx b/frontend/src/pages/System/Logs/index.test.tsx
new file mode 100644
index 000000000..b70336b79
--- /dev/null
+++ b/frontend/src/pages/System/Logs/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemLogsView from ".";
+
+describe("System Logs", () => {
+ it("should render with logs", async () => {
+ server.use(
+ http.get("/api/system/logs", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Providers/index.test.tsx b/frontend/src/pages/System/Providers/index.test.tsx
new file mode 100644
index 000000000..2e2f4ffdc
--- /dev/null
+++ b/frontend/src/pages/System/Providers/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemProvidersView from ".";
+
+describe("System Providers", () => {
+ it("should render with providers", async () => {
+ server.use(
+ http.get("/api/providers", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Releases/index.test.tsx b/frontend/src/pages/System/Releases/index.test.tsx
new file mode 100644
index 000000000..6e8dfed27
--- /dev/null
+++ b/frontend/src/pages/System/Releases/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemReleasesView from ".";
+
+describe("System Releases", () => {
+ it("should render with releases", async () => {
+ server.use(
+ http.get("/api/system/releases", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Status/index.test.tsx b/frontend/src/pages/System/Status/index.test.tsx
new file mode 100644
index 000000000..988114d2e
--- /dev/null
+++ b/frontend/src/pages/System/Status/index.test.tsx
@@ -0,0 +1,29 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemStatusView from ".";
+
+describe("System Status", () => {
+ it("should render with status", async () => {
+ server.use(
+ http.get("/api/system/status", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ server.use(
+ http.get("/api/system/health", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/Tasks/index.test.tsx b/frontend/src/pages/System/Tasks/index.test.tsx
new file mode 100644
index 000000000..b6c3710af
--- /dev/null
+++ b/frontend/src/pages/System/Tasks/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import SystemTasksView from ".";
+
+describe("System Tasks", () => {
+ it("should render with tasks", async () => {
+ server.use(
+ http.get("/api/system/tasks", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/System/system.test.tsx b/frontend/src/pages/System/system.test.tsx
deleted file mode 100644
index 813654a7b..000000000
--- a/frontend/src/pages/System/system.test.tsx
+++ /dev/null
@@ -1,41 +0,0 @@
-import SystemAnnouncementsView from "@/pages/System/Announcements";
-import { renderTest, RenderTestCase } from "@/tests/render";
-import SystemBackupsView from "./Backups";
-import SystemLogsView from "./Logs";
-import SystemProvidersView from "./Providers";
-import SystemReleasesView from "./Releases";
-import SystemStatusView from "./Status";
-import SystemTasksView from "./Tasks";
-
-const cases: RenderTestCase[] = [
- {
- name: "backups page",
- ui: SystemBackupsView,
- },
- {
- name: "logs page",
- ui: SystemLogsView,
- },
- {
- name: "providers page",
- ui: SystemProvidersView,
- },
- {
- name: "releases page",
- ui: SystemReleasesView,
- },
- {
- name: "status page",
- ui: SystemStatusView,
- },
- {
- name: "tasks page",
- ui: SystemTasksView,
- },
- {
- name: "announcements page",
- ui: SystemAnnouncementsView,
- },
-];
-
-renderTest("System", cases);
diff --git a/frontend/src/pages/Wanted/Movies/index.test.tsx b/frontend/src/pages/Wanted/Movies/index.test.tsx
new file mode 100644
index 000000000..8974573a5
--- /dev/null
+++ b/frontend/src/pages/Wanted/Movies/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import WantedMoviesView from ".";
+
+describe("Wanted Movies", () => {
+ it("should render with wanted movies", async () => {
+ server.use(
+ http.get("/api/movies/wanted", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/Wanted/Series/index.test.tsx b/frontend/src/pages/Wanted/Series/index.test.tsx
new file mode 100644
index 000000000..8387dabb9
--- /dev/null
+++ b/frontend/src/pages/Wanted/Series/index.test.tsx
@@ -0,0 +1,21 @@
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { render } from "@/tests";
+import server from "@/tests/mocks/node";
+import WantedSeriesView from ".";
+
+describe("Wanted Series", () => {
+ it("should render with wanted series", async () => {
+ server.use(
+ http.get("/api/episodes/wanted", () => {
+ return HttpResponse.json({
+ data: [],
+ });
+ }),
+ );
+
+ render();
+
+ // TODO: Assert
+ });
+});
diff --git a/frontend/src/pages/Wanted/wanted.test.tsx b/frontend/src/pages/Wanted/wanted.test.tsx
deleted file mode 100644
index 36e72c4bb..000000000
--- a/frontend/src/pages/Wanted/wanted.test.tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { renderTest, RenderTestCase } from "@/tests/render";
-import WantedMoviesView from "./Movies";
-import WantedSeriesView from "./Series";
-
-const cases: RenderTestCase[] = [
- {
- name: "movie page",
- ui: WantedMoviesView,
- },
- {
- name: "series page",
- ui: WantedSeriesView,
- },
-];
-
-renderTest("Wanted", cases);
diff --git a/frontend/src/pages/views/MassEditor.tsx b/frontend/src/pages/views/MassEditor.tsx
index eac9dcb94..57221888a 100644
--- a/frontend/src/pages/views/MassEditor.tsx
+++ b/frontend/src/pages/views/MassEditor.tsx
@@ -57,14 +57,6 @@ function MassEditor(props: MassEditorProps) {
];
}, [profileOptions.options]);
- const getKey = useCallback((value: Language.Profile | null) => {
- if (value) {
- return value.name;
- }
-
- return "Clear";
- }, []);
-
const { mutateAsync } = mutation;
/**
@@ -136,7 +128,6 @@ function MassEditor(props: MassEditorProps) {
placeholder="Change Profile"
withCheckIcon={false}
options={profileOptionsWithAction}
- getkey={getKey}
disabled={selections.length === 0}
comboboxProps={{
store: combobox,
diff --git a/frontend/src/tests/mocks/node.ts b/frontend/src/tests/mocks/node.ts
new file mode 100644
index 000000000..7826431e6
--- /dev/null
+++ b/frontend/src/tests/mocks/node.ts
@@ -0,0 +1,5 @@
+import { setupServer } from "msw/node";
+
+const server = setupServer();
+
+export default server;
diff --git a/frontend/src/tests/render.tsx b/frontend/src/tests/render.tsx
index e0031e903..991c8c9b9 100644
--- a/frontend/src/tests/render.tsx
+++ b/frontend/src/tests/render.tsx
@@ -4,10 +4,17 @@ import { render } from ".";
export interface RenderTestCase {
name: string;
ui: FunctionComponent;
+ setupEach?: () => void;
}
export function renderTest(name: string, cases: RenderTestCase[]) {
describe(name, () => {
+ beforeEach(() => {
+ cases.forEach((element) => {
+ element.setupEach?.();
+ });
+ });
+
cases.forEach((element) => {
it(`${element.name.toLowerCase()} should render`, () => {
render();
diff --git a/frontend/src/tests/setup.ts b/frontend/src/tests/setup.ts
deleted file mode 100644
index 0f6df68ba..000000000
--- a/frontend/src/tests/setup.ts
+++ /dev/null
@@ -1,30 +0,0 @@
-/* eslint-disable @typescript-eslint/no-empty-function */
-
-import { vitest } from "vitest";
-import "@testing-library/jest-dom";
-
-// From https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function
-Object.defineProperty(window, "matchMedia", {
- writable: true,
- value: vitest.fn().mockImplementation((query) => ({
- matches: false,
- media: query,
- onchange: null,
- addListener: vitest.fn(), // Deprecated
- removeListener: vitest.fn(), // Deprecated
- addEventListener: vitest.fn(),
- removeEventListener: vitest.fn(),
- dispatchEvent: vitest.fn(),
- })),
-});
-
-// From https://github.com/mantinedev/mantine/blob/master/configuration/jest/jsdom.mocks.js
-class ResizeObserver {
- observe() {}
- unobserve() {}
- disconnect() {}
-}
-
-window.ResizeObserver = ResizeObserver;
-
-window.scrollTo = () => {};
diff --git a/frontend/src/tests/setup.tsx b/frontend/src/tests/setup.tsx
new file mode 100644
index 000000000..52310b906
--- /dev/null
+++ b/frontend/src/tests/setup.tsx
@@ -0,0 +1,70 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+
+import { http } from "msw";
+import { HttpResponse } from "msw";
+import { vi, vitest } from "vitest";
+import "@testing-library/jest-dom";
+import queryClient from "@/apis/queries";
+import server from "./mocks/node";
+
+vi.mock("recharts", async () => {
+ const OriginalRechartsModule = await vi.importActual("recharts");
+
+ return {
+ ...OriginalRechartsModule,
+ ResponsiveContainer: ({ children }: { children: React.ReactNode }) => (
+ {children}
+ ),
+ };
+});
+
+// From https://stackoverflow.com/questions/39830580/jest-test-fails-typeerror-window-matchmedia-is-not-a-function
+Object.defineProperty(window, "matchMedia", {
+ writable: true,
+ value: vitest.fn().mockImplementation((query) => ({
+ matches: false,
+ media: query,
+ onchange: null,
+ addListener: vitest.fn(), // Deprecated
+ removeListener: vitest.fn(), // Deprecated
+ addEventListener: vitest.fn(),
+ removeEventListener: vitest.fn(),
+ dispatchEvent: vitest.fn(),
+ })),
+});
+
+// From https://github.com/mantinedev/mantine/blob/master/configuration/jest/jsdom.mocks.js
+class ResizeObserver {
+ observe() {}
+ unobserve() {}
+ disconnect() {}
+}
+
+window.ResizeObserver = ResizeObserver;
+
+window.scrollTo = () => {};
+
+beforeAll(() => {
+ server.listen({ onUnhandledRequest: "error" });
+});
+
+beforeEach(() => {
+ server.resetHandlers();
+ server.use(
+ http.get("/api/system/settings", () => {
+ return HttpResponse.json({
+ general: {
+ theme: "auto",
+ },
+ });
+ }),
+ );
+});
+
+afterEach(() => {
+ server.resetHandlers();
+
+ queryClient.clear();
+});
+
+afterAll(() => server.close());
diff --git a/frontend/src/utilities/console.ts b/frontend/src/utilities/console.ts
index 64da1330c..372207db3 100644
--- a/frontend/src/utilities/console.ts
+++ b/frontend/src/utilities/console.ts
@@ -6,6 +6,10 @@ import { isProdEnv } from ".";
type LoggerType = "info" | "warning" | "error";
export function LOG(type: LoggerType, msg: string, ...payload: any[]) {
+ if (import.meta.env.MODE === "test") {
+ return;
+ }
+
if (!isProdEnv) {
let logger = console.log;
if (type === "warning") {
diff --git a/frontend/vite.config.ts b/frontend/vite.config.ts
index fbae83c88..3eecb807d 100644
--- a/frontend/vite.config.ts
+++ b/frontend/vite.config.ts
@@ -132,7 +132,7 @@ export default defineConfig(async ({ mode, command }) => {
test: {
globals: true,
environment: "jsdom",
- setupFiles: "./src/tests/setup.ts",
+ setupFiles: "./src/tests/setup.tsx",
},
server: {
proxy: {