diff --git a/Dockerfile b/Dockerfile index 2cada3c37..9bdc22f3f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -211,7 +211,11 @@ RUN rm /frontend/src/api/omni/specs/virtual.proto RUN rm /frontend/src/api/omni/specs/ephemeral.proto # runs js unit-tests -FROM js AS unit-tests-frontend +# TODO: Implement in kres +FROM mcr.microsoft.com/playwright:v1.60.0-noble AS unit-tests-frontend +COPY --from=js /src /src +WORKDIR /src +RUN --mount=type=cache,target=/root/.npm,id=omni/root/.npm,sharing=locked npm ci --engine-strict=false RUN CI=true npm test # tools and sources diff --git a/frontend/.gitignore b/frontend/.gitignore index d2f6d8c9b..839866795 100644 --- a/frontend/.gitignore +++ b/frontend/.gitignore @@ -18,6 +18,10 @@ dist-ssr /blob-report/ /playwright/.cache/ +# Vitest browser mode artifacts +.vitest-attachments/ +**/__screenshots__/ + # Storybook *storybook.log storybook-static diff --git a/frontend/.prettierignore b/frontend/.prettierignore index aaee7700f..d103ee8a7 100644 --- a/frontend/.prettierignore +++ b/frontend/.prettierignore @@ -1,6 +1,6 @@ src/api/**/*.pb.ts src/api/resources.ts src/schemas/*.schema.json -.storybook/public/mockServiceWorker.js +**/mockServiceWorker.js typed-router.d.ts eslint-suppressions.json diff --git a/frontend/.storybook/public/mockServiceWorker.js b/frontend/.storybook/public/mockServiceWorker.js index 9cd84013c..33dde9e77 100644 --- a/frontend/.storybook/public/mockServiceWorker.js +++ b/frontend/.storybook/public/mockServiceWorker.js @@ -7,7 +7,7 @@ * - Please do NOT modify this file. */ -const PACKAGE_VERSION = '2.14.5' +const PACKAGE_VERSION = '2.14.6' const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') const activeClientIds = new Set() diff --git a/frontend/eslint.config.ts b/frontend/eslint.config.ts index b710f8783..ee7768ce0 100644 --- a/frontend/eslint.config.ts +++ b/frontend/eslint.config.ts @@ -41,7 +41,7 @@ export default defineConfigWithVueTs( }, }, // By default, ESLint ignores all dot-files - { ignores: ['!.storybook', '.storybook/public/mockServiceWorker.js'] }, + { ignores: ['!.storybook', '**/mockServiceWorker.js'] }, ...storybook.configs['flat/recommended'], ...storybook.configs['flat/csf-strict'], diff --git a/frontend/msw/server.ts b/frontend/msw/server.ts index ad7a54777..8af2767c4 100644 --- a/frontend/msw/server.ts +++ b/frontend/msw/server.ts @@ -3,27 +3,27 @@ // Use of this software is governed by the Business Source License // included in the LICENSE file. import { http, HttpResponse } from 'msw' -import { setupServer } from 'msw/node' +import { setupWorker } from 'msw/browser' import type { Resource } from '@/api/grpc' import type { GetRequest, GetResponse } from '@/api/omni/resources/resources.pb' import { createWatchStreamHandler, type WatchStreamHandlerOptions } from './helpers' -export const server = setupServer() +export const worker = setupWorker() export function createWatchStreamMock( options?: WatchStreamHandlerOptions, ) { const { handler, pushEvents, closeStream } = createWatchStreamHandler(options) - server.use(handler) + worker.use(handler) return { pushEvents, closeStream } } export function createGetMock() { - server.use( + worker.use( http.post('/omni.resources.ResourceService/Get', () => { return HttpResponse.json( { body: JSON.stringify({ spec: {}, metadata: {} } satisfies Resource) }, diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 38e3de66a..30c0991d2 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -52,18 +52,17 @@ "@storybook/addon-a11y": "^10.3.6", "@storybook/vue3-vite": "^10.3.6", "@tailwindcss/vite": "^4.3.0", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/user-event": "^14.6.1", "@testing-library/vue": "^8.1.0", "@tsconfig/node24": "^24.0.4", "@types/js-yaml": "^4.0.9", - "@types/jsdom": "^28.0.1", "@types/lodash": "^4.17.24", "@types/luxon": "^3.7.1", "@types/node": "^24.12.3", "@types/pluralize": "^0.0.33", "@types/semver": "^7.7.1", "@vitejs/plugin-vue": "^6.0.6", + "@vitest/browser": "^4.1.6", + "@vitest/browser-playwright": "^4.1.6", "@vitest/eslint-plugin": "^1.6.17", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -75,11 +74,9 @@ "eslint-plugin-simple-import-sort": "^13.0.0", "eslint-plugin-storybook": "^10.3.6", "eslint-plugin-vue": "^10.9.1", - "fake-indexeddb": "^6.2.5", - "jsdom": "^29.1.1", "json-diff-ts": "^4.10.4", "kubernetes-types": "^1.30.0", - "msw": "^2.14.5", + "msw": "^2.14.6", "msw-storybook-addon": "^2.0.7", "openpgp": "^6.3.0", "prettier": "^3.8.3", @@ -91,10 +88,11 @@ "typescript": "^6.0.3", "vite": "^8.0.12", "vite-plugin-vue-devtools": "^8.1.2", - "vitest": "^4.1.5", + "vitest": "^4.1.6", + "vitest-browser-vue": "^2.1.0", "vue-component-type-helpers": "^3.2.8", "vue-tsc": "^3.2.8", - "yaml": "^2.8.4" + "yaml": "^2.9.0" }, "engines": { "node": "^24.15.0" @@ -107,57 +105,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@asamuzakjp/css-color": { - "version": "5.1.11", - "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-5.1.11.tgz", - "integrity": "sha512-KVw6qIiCTUQhByfTd78h2yD1/00waTmm9uy/R7Ck/ctUyAPj+AEDLkQIdJW0T8+qGgj3j5bpNKK7Q3G+LedJWg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@csstools/css-calc": "^3.2.0", - "@csstools/css-color-parser": "^4.1.0", - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/dom-selector": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/dom-selector/-/dom-selector-7.1.1.tgz", - "integrity": "sha512-67RZDnYRc8H/8MLDgQCDE//zoqVFwajkepHZgmXrbwybzXOEwOWGPYGmALYl9J2DOLfFPPs6kKCqmbzV895hTQ==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/generational-cache": "^1.0.1", - "@asamuzakjp/nwsapi": "^2.3.9", - "bidi-js": "^1.0.3", - "css-tree": "^3.2.1", - "is-potential-custom-element-name": "^1.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/generational-cache": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@asamuzakjp/generational-cache/-/generational-cache-1.0.1.tgz", - "integrity": "sha512-wajfB8KqzMCN2KGNFdLkReeHncd0AslUSrvHVvvYWuU8ghncRJoA50kT3zP9MVL0+9g4/67H+cdvBskj9THPzg==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, - "node_modules/@asamuzakjp/nwsapi": { - "version": "2.3.9", - "resolved": "https://registry.npmjs.org/@asamuzakjp/nwsapi/-/nwsapi-2.3.9.tgz", - "integrity": "sha512-n8GuYSrI9bF7FFZ/SjhwevlHc8xaVlb/7HmHelnc/PZXBD2ZR49NnN9sMMuDdEGPeeRQ5d0hqlSlEpgCX3Wl0Q==", - "dev": true, - "license": "MIT" - }, "node_modules/@auth0/auth0-spa-js": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/@auth0/auth0-spa-js/-/auth0-spa-js-2.11.0.tgz", @@ -691,18 +638,12 @@ "node": ">=6.9.0" } }, - "node_modules/@bramus/specificity": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/@bramus/specificity/-/specificity-2.4.2.tgz", - "integrity": "sha512-ctxtJ/eA+t+6q2++vj5j7FYX3nRu311q1wfYH3xjlLOsczhlhxAg2FWNUXhpGvAw3BWo1xBcvOV6/YLc2r5FJw==", + "node_modules/@blazediff/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@blazediff/core/-/core-1.9.1.tgz", + "integrity": "sha512-ehg3jIkYKulZh+8om/O25vkvSsXXwC+skXmyA87FFx6A/45eqOkZsBltMw/TVteb0mloiGT8oGRTcjRAz66zaA==", "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^3.0.0" - }, - "bin": { - "specificity": "bin/cli.js" - } + "license": "MIT" }, "node_modules/@chromatic-com/storybook": { "version": "5.1.2", @@ -725,146 +666,6 @@ "storybook": "^0.0.0-0 || ^10.1.0 || ^10.1.0-0 || ^10.2.0-0 || ^10.3.0-0 || ^10.4.0-0" } }, - "node_modules/@csstools/color-helpers": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-6.0.2.tgz", - "integrity": "sha512-LMGQLS9EuADloEFkcTBR3BwV/CGHV7zyDxVRtVDTwdI2Ca4it0CCVTT9wCkxSgokjE5Ho41hEPgb8OEUwoXr6Q==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=20.19.0" - } - }, - "node_modules/@csstools/css-calc": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-3.2.0.tgz", - "integrity": "sha512-bR9e6o2BDB12jzN/gIbjHa5wLJ4UjD1CB9pM7ehlc0ddk6EBz+yYS1EV2MF55/HUxrHcB/hehAyt5vhsA3hx7w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-4.1.0.tgz", - "integrity": "sha512-U0KhLYmy2GVj6q4T3WaAe6NPuFYCPQoE3b0dRGxejWDgcPp8TP7S5rVdM5ZrFaqu4N67X8YaPBw14dQSYx3IyQ==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^6.0.2", - "@csstools/css-calc": "^3.2.0" - }, - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^4.0.0", - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-4.0.0.tgz", - "integrity": "sha512-+B87qS7fIG3L5h3qwJ/IFbjoVoOe/bpOdh9hAjXbvx0o8ImEmUsGXN0inFOnk2ChCFgqkkGFQ+TpM5rbhkKe4w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^4.0.0" - } - }, - "node_modules/@csstools/css-syntax-patches-for-csstree": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.1.3.tgz", - "integrity": "sha512-SH60bMfrRCJF3morcdk57WklujF4Jr/EsQUzqkarfHXEFcAR1gg7fS/chAE922Sehgzc1/+Tz5H3Ypa1HiEKrg==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "peerDependencies": { - "css-tree": "^3.2.1" - }, - "peerDependenciesMeta": { - "css-tree": { - "optional": true - } - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-4.0.0.tgz", - "integrity": "sha512-QxULHAm7cNu72w97JUNCBFODFaXpbDg+dP8b/oWFAZ2MTRppA3U00Y2L1HqaS4J6yBqxwa/Y3nMBaxVKbB/NsA==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=20.19.0" - } - }, "node_modules/@emnapi/core": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", @@ -1474,24 +1275,6 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, - "node_modules/@exodus/bytes": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@exodus/bytes/-/bytes-1.15.0.tgz", - "integrity": "sha512-UY0nlA+feH81UGSHv92sLEPLCeZFjXOuHhrIo0HQydScuQc8s0A7kL/UdgwgDq8g8ilksmuoF35YVTNphV2aBQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - }, - "peerDependencies": { - "@noble/hashes": "^1.8.0 || ^2.0.0" - }, - "peerDependenciesMeta": { - "@noble/hashes": { - "optional": true - } - } - }, "node_modules/@faker-js/faker": { "version": "10.4.0", "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-10.4.0.tgz", @@ -2991,26 +2774,6 @@ "dev": true, "license": "MIT" }, - "node_modules/@types/jsdom": { - "version": "28.0.1", - "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-28.0.1.tgz", - "integrity": "sha512-GJq2QE4TAZ5ajSoCasn5DOFm8u1mI3tIFvM5tIq3W5U/RTB6gsHwc6Yhpl91X9VSDOUVblgXmG+2+sSvFQrdlw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tough-cookie": "*", - "parse5": "^7.0.0", - "undici-types": "^7.21.0" - } - }, - "node_modules/@types/jsdom/node_modules/undici-types": { - "version": "7.22.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.22.0.tgz", - "integrity": "sha512-RKZvifiL60xdsIuC80UY0dq8Z7DbJUV8/l2hOVbyZAxBzEeQU4Z58+4ZzJ6WN2Lidi9KzT5EbiGX+PI/UGYuRw==", - "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", @@ -3072,13 +2835,6 @@ "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", @@ -3391,6 +3147,101 @@ "vue": "^3.2.25" } }, + "node_modules/@vitest/browser": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/browser/-/browser-4.1.6.tgz", + "integrity": "sha512-ynsspTubXGSpa58JFJ24xIQt4z4A25epSbugEyaTmmrV1//Wec9EgE/LtoaC6yxUrXi5P7erGHRrkdZIHaVQuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@blazediff/core": "1.9.1", + "@vitest/mocker": "4.1.6", + "@vitest/utils": "4.1.6", + "magic-string": "^0.30.21", + "pngjs": "^7.0.0", + "sirv": "^3.0.2", + "tinyrainbow": "^3.1.0", + "ws": "^8.19.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "4.1.6" + } + }, + "node_modules/@vitest/browser-playwright": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/browser-playwright/-/browser-playwright-4.1.6.tgz", + "integrity": "sha512-4csoeyl/qwHyxU2zNL0++WaoDr8YJDXOQPwWPNJoTZ+QzcdO3INYKgF5Zfz730Io7zbkuv914aZmfQ+QE+1Hvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/browser": "4.1.6", + "@vitest/mocker": "4.1.6", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "playwright": "*", + "vitest": "4.1.6" + }, + "peerDependenciesMeta": { + "playwright": { + "optional": false + } + } + }, + "node_modules/@vitest/browser-playwright/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/pretty-format": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/@vitest/utils": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.6", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/browser/node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/@vitest/eslint-plugin": { "version": "1.6.17", "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.6.17.tgz", @@ -3440,13 +3291,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.5.tgz", - "integrity": "sha512-/x2EmFC4mT4NNzqvC3fmesuV97w5FC903KPmey4gsnJiMQ3Be1IlDKVaDaG8iqaLFHqJ2FVEkxZk5VmeLjIItw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.6.tgz", + "integrity": "sha512-MCFc63czMjEInOlcY2cpQCvCN+KgbAn+60xu9cMgP4sKaLC5JNAKw7JH8QdAnoAC88hW1IiSNZ+GgVXlN1UcMQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.5", + "@vitest/spy": "4.1.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -3467,9 +3318,9 @@ } }, "node_modules/@vitest/mocker/node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -3490,13 +3341,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.5.tgz", - "integrity": "sha512-2D+o7Pr82IEO46YPpoA/YU0neeyr6FTerQb5Ro7BUnBuv6NQtT/kmVnczngiMEBhzgqz2UZYl5gArejsyERDSQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.6.tgz", + "integrity": "sha512-nOPCmn2+yD0ZNmKdsXGv/UxMMWbMuKeD6GyYncNwdkYDxpQvrPSKYj2rWuDjC2Y4b6w6hjip5dBKFzEUuZe3vA==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.5", + "@vitest/utils": "4.1.6", "pathe": "^2.0.3" }, "funding": { @@ -3504,9 +3355,9 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -3517,13 +3368,13 @@ } }, "node_modules/@vitest/runner/node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -3542,14 +3393,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.5.tgz", - "integrity": "sha512-zypXEt4KH/XgKGPUz4eC2AvErYx0My5hfL8oDb1HzGFpEk1P62bxSohdyOmvz+d9UJwanI68MKwr2EquOaOgMQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.6.tgz", + "integrity": "sha512-YhsdE6xAVfTDmzjxL2ZDUvjj+ZsgyOKe+TdQzqkD72wIOmHka8NuGQ6NpTNZv9D2Z63fbwWKJPeVpEw4EQgYxw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/pretty-format": "4.1.6", + "@vitest/utils": "4.1.6", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -3558,9 +3409,9 @@ } }, "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -3571,13 +3422,13 @@ } }, "node_modules/@vitest/snapshot/node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -4462,16 +4313,6 @@ "node": ">=6.0.0" } }, - "node_modules/bidi-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz", - "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==", - "dev": true, - "license": "MIT", - "dependencies": { - "require-from-string": "^2.0.2" - } - }, "node_modules/birpc": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/birpc/-/birpc-2.9.0.tgz", @@ -4921,20 +4762,6 @@ "node": ">= 8" } }, - "node_modules/css-tree": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", - "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.27.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, "node_modules/css.escape": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/css.escape/-/css.escape-1.5.1.tgz", @@ -4961,20 +4788,6 @@ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", "license": "MIT" }, - "node_modules/data-urls": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-7.0.0.tgz", - "integrity": "sha512-23XHcCF+coGYevirZceTVD7NdJOqVn+49IHyxgszm+JIiHLoB2TkmPtsYkNWT1pvRSGkc35L6NHs0yHkN2SumA==", - "dev": true, - "license": "MIT", - "dependencies": { - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/date-fns": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz", @@ -5010,13 +4823,6 @@ } } }, - "node_modules/decimal.js": { - "version": "10.6.0", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", - "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", - "dev": true, - "license": "MIT" - }, "node_modules/deep-eql": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", @@ -5312,19 +5118,6 @@ "node": ">=10.13.0" } }, - "node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, "node_modules/error-stack-parser-es": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", @@ -5874,16 +5667,6 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, - "node_modules/fake-indexeddb": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/fake-indexeddb/-/fake-indexeddb-6.2.5.tgz", - "integrity": "sha512-CGnyrvbhPlWYMngksqrSSUT1BAVP49dZocrHuK0SvtR0D5TMs5wP0o3j7jexDJW01KSadjBp1M/71o/KR3nD1w==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6409,19 +6192,6 @@ "integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==", "license": "MIT" }, - "node_modules/html-encoding-sniffer": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-6.0.0.tgz", - "integrity": "sha512-CV9TW3Y3f8/wT0BRFc1/KAVQ3TUHiXmaAb6VW9vtiMFf7SLoMd1PdAc4W3KFOFETBJUb90KatHqlsZMWV+R9Gg==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.6.0" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/idb-keyval": { "version": "6.2.2", "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.2.2.tgz", @@ -6742,13 +6512,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/is-potential-custom-element-name": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true, - "license": "MIT" - }, "node_modules/is-promise": { "version": "2.2.2", "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz", @@ -7003,83 +6766,6 @@ "js-yaml": "bin/js-yaml.js" } }, - "node_modules/jsdom": { - "version": "29.1.1", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-29.1.1.tgz", - "integrity": "sha512-ECi4Fi2f7BdJtUKTflYRTiaMxIB0O6zfR1fX0GXpUrf6flp8QIYn1UT20YQqdSOfk2dfkCwS8LAFoJDEppNK5Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@asamuzakjp/css-color": "^5.1.11", - "@asamuzakjp/dom-selector": "^7.1.1", - "@bramus/specificity": "^2.4.2", - "@csstools/css-syntax-patches-for-csstree": "^1.1.3", - "@exodus/bytes": "^1.15.0", - "css-tree": "^3.2.1", - "data-urls": "^7.0.0", - "decimal.js": "^10.6.0", - "html-encoding-sniffer": "^6.0.0", - "is-potential-custom-element-name": "^1.0.1", - "lru-cache": "^11.3.5", - "parse5": "^8.0.1", - "saxes": "^6.0.0", - "symbol-tree": "^3.2.4", - "tough-cookie": "^6.0.1", - "undici": "^7.25.0", - "w3c-xmlserializer": "^5.0.0", - "webidl-conversions": "^8.0.1", - "whatwg-mimetype": "^5.0.0", - "whatwg-url": "^16.0.1", - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": "^20.19.0 || ^22.13.0 || >=24.0.0" - }, - "peerDependencies": { - "canvas": "^3.0.0" - }, - "peerDependenciesMeta": { - "canvas": { - "optional": true - } - } - }, - "node_modules/jsdom/node_modules/entities": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz", - "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20.19.0" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/parse5": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz", - "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^8.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/jsdom/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, "node_modules/jsesc": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", @@ -7516,16 +7202,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lru-cache": { - "version": "11.3.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", - "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", - "dev": true, - "license": "BlueOak-1.0.0", - "engines": { - "node": "20 || >=22" - } - }, "node_modules/luxon": { "version": "3.7.2", "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", @@ -7591,13 +7267,6 @@ "node": ">= 0.4" } }, - "node_modules/mdn-data": { - "version": "2.27.1", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", - "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", - "dev": true, - "license": "CC0-1.0" - }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7807,9 +7476,9 @@ "license": "MIT" }, "node_modules/msw": { - "version": "2.14.5", - "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.5.tgz", - "integrity": "sha512-X6G05oX4x0e+CNI55KMdhMmwHCBKf2iwazGr+iwsdoJ94JA1ED7wSXb6V+lLPdqFkmIlPiGYvayqnaNcOzobDA==", + "version": "2.14.6", + "resolved": "https://registry.npmjs.org/msw/-/msw-2.14.6.tgz", + "integrity": "sha512-ALe+N10S72cyx94cMcy3Zs4HhXCj35sgeAL4c+WTvKi0zWnbd8/h0lcFqv0mb2P+aSgAdD7p9HzvA0DiUPxsyg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -8151,19 +7820,6 @@ "dev": true, "license": "BlueOak-1.0.0" }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "dev": true, - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, "node_modules/path-browserify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-1.0.1.tgz", @@ -8320,6 +7976,16 @@ "node": ">=4" } }, + "node_modules/pngjs": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-7.0.0.tgz", + "integrity": "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.19.0" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -8854,6 +8520,7 @@ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "license": "MIT", + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8993,19 +8660,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/saxes": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", - "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", - "dev": true, - "license": "ISC", - "dependencies": { - "xmlchars": "^2.2.0" - }, - "engines": { - "node": ">=v12.22.7" - } - }, "node_modules/scheduler": { "version": "0.27.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", @@ -9471,13 +9125,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/symbol-tree": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", - "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true, - "license": "MIT" - }, "node_modules/synckit": { "version": "0.11.11", "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", @@ -9661,19 +9308,6 @@ "node": ">=16" } }, - "node_modules/tr46": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-6.0.0.tgz", - "integrity": "sha512-bLVMLPtstlZ4iMQHpFHTR7GAGj2jxi8Dg0s2h2MafAE4uSWF98FC/3MomU51iQAMf8/qDUbKWf5GxuvvVcXEhw==", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.3.1" - }, - "engines": { - "node": ">=20" - } - }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9841,16 +9475,6 @@ "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, - "node_modules/undici": { - "version": "7.25.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.25.0.tgz", - "integrity": "sha512-xXnp4kTyor2Zq+J1FfPI6Eq3ew5h6Vl0F/8d9XU5zZQf1tX9s2Su1/3PiMmUANFULpmksxkClamIZcaUqryHsQ==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20.18.1" - } - }, "node_modules/undici-types": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", @@ -10222,19 +9846,19 @@ } }, "node_modules/vitest": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.5.tgz", - "integrity": "sha512-9Xx1v3/ih3m9hN+SbfkUyy0JAs72ap3r7joc87XL6jwF0jGg6mFBvQ1SrwaX+h8BlkX6Hz9shdd1uo6AF+ZGpg==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.6.tgz", + "integrity": "sha512-6lvjbS3p9b4CrdCmguzbh2/4uoXhGE2q71R4OX5sqF9R1bo9Xd6fGrMAfvp5wnCzlBnFVdCOp6onuTQVbo8iUQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.5", - "@vitest/mocker": "4.1.5", - "@vitest/pretty-format": "4.1.5", - "@vitest/runner": "4.1.5", - "@vitest/snapshot": "4.1.5", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/expect": "4.1.6", + "@vitest/mocker": "4.1.6", + "@vitest/pretty-format": "4.1.6", + "@vitest/runner": "4.1.6", + "@vitest/snapshot": "4.1.6", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -10262,12 +9886,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.5", - "@vitest/browser-preview": "4.1.5", - "@vitest/browser-webdriverio": "4.1.5", - "@vitest/coverage-istanbul": "4.1.5", - "@vitest/coverage-v8": "4.1.5", - "@vitest/ui": "4.1.5", + "@vitest/browser-playwright": "4.1.6", + "@vitest/browser-preview": "4.1.6", + "@vitest/browser-webdriverio": "4.1.6", + "@vitest/coverage-istanbul": "4.1.6", + "@vitest/coverage-v8": "4.1.6", + "@vitest/ui": "4.1.6", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -10311,17 +9935,34 @@ } } }, + "node_modules/vitest-browser-vue": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/vitest-browser-vue/-/vitest-browser-vue-2.1.0.tgz", + "integrity": "sha512-K3H/oxIOY4EjXx2bxwrLsPg4jTMvzpRW6Jb6T8XZRoxUvmDVwGkd8mua130F8GZezE7H5QYoXd/S8hYijw7j5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vue/test-utils": "^2.4.6" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "vitest": "^4.0.0-0", + "vue": "^3.0.0" + } + }, "node_modules/vitest/node_modules/@vitest/expect": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.5.tgz", - "integrity": "sha512-PWBaRY5JoKuRnHlUHfpV/KohFylaDZTupcXN1H9vYryNLOnitSw60Mw9IAE2r67NbwwzBw/Cc/8q9BK3kIX8Kw==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.6.tgz", + "integrity": "sha512-7EHDquPthALSV0jhhjgEW8FXaviMx7rSqu8W6oqCoAuOhKov814P99QDV1pxMA3QPv21YudvJngIhjrNI4opLg==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.5", - "@vitest/utils": "4.1.5", + "@vitest/spy": "4.1.6", + "@vitest/utils": "4.1.6", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -10330,9 +9971,9 @@ } }, "node_modules/vitest/node_modules/@vitest/pretty-format": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.5.tgz", - "integrity": "sha512-7I3q6l5qr03dVfMX2wCo9FxwSJbPdwKjy2uu/YPpU3wfHvIL4QHwVRp57OfGrDFeUJ8/8QdfBKIV12FTtLn00g==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.6.tgz", + "integrity": "sha512-h5SxD/IzNhZYnrSZRsUZQIC+vD0GY8cUvq0iwsmkFKixRCKLLWqCXa/FIQ4S1R+sI+PGoojkHsdNrbZiM9Qpgw==", "dev": true, "license": "MIT", "dependencies": { @@ -10343,9 +9984,9 @@ } }, "node_modules/vitest/node_modules/@vitest/spy": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.5.tgz", - "integrity": "sha512-2lNOsh6+R2Idnf1TCZqSwYlKN2E/iDlD8sgU59kYVl+OMDmvldO1VDk39smRfpUNwYpNRVn3w4YfuC7KfbBnkQ==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.6.tgz", + "integrity": "sha512-JFKxMx6udhwKh/Ldo270e17QX710vgunMkuPAvXjHSvC6oqLWAHhVhjg/I71q0u0CBSErIODV1Kjv0FQNSWjdg==", "dev": true, "license": "MIT", "funding": { @@ -10353,13 +9994,13 @@ } }, "node_modules/vitest/node_modules/@vitest/utils": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.5.tgz", - "integrity": "sha512-76wdkrmfXfqGjueGgnb45ITPyUi1ycZ4IHgC2bhPDUfWHklY/q3MdLOAB+TF1e6xfl8NxNY0ZYaPCFNWSsw3Ug==", + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.6.tgz", + "integrity": "sha512-FxIY+U81R3LGKCxaHHFRQ5+g6/iRgGLmeHWdp2Amj4ljQRrEIWHmZyDfDYBRZlpyqA7qKxtS9DD1dhk8RnRIVQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.5", + "@vitest/pretty-format": "4.1.6", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -10793,39 +10434,6 @@ } } }, - "node_modules/w3c-xmlserializer": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", - "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", - "dev": true, - "license": "MIT", - "dependencies": { - "xml-name-validator": "^5.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/w3c-xmlserializer/node_modules/xml-name-validator": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", - "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", - "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=18" - } - }, - "node_modules/webidl-conversions": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-8.0.1.tgz", - "integrity": "sha512-BMhLD/Sw+GbJC21C/UgyaZX41nPt8bUTg+jWyDeg7e7YN4xOM05YPSIXceACnXVtqyEw/LMClUQMtMZ+PGGpqQ==", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=20" - } - }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -10838,31 +10446,6 @@ "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==", "license": "MIT" }, - "node_modules/whatwg-mimetype": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-5.0.0.tgz", - "integrity": "sha512-sXcNcHOC51uPGF0P/D4NVtrkjSU2fNsm9iog4ZvZJsL3rjoDAzXZhkm2MWt1y+PUdggKAYVoMAIYcs78wJ51Cw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=20" - } - }, - "node_modules/whatwg-url": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-16.0.1.tgz", - "integrity": "sha512-1to4zXBxmXHV3IiSSEInrreIlu02vUOvrhxJJH5vcxYTBDAx51cqZiKdyTxlecdKNSjj8EcxGBxNf6Vg+945gw==", - "dev": true, - "license": "MIT", - "dependencies": { - "@exodus/bytes": "^1.11.0", - "tr46": "^6.0.0", - "webidl-conversions": "^8.0.1" - }, - "engines": { - "node": "^20.19.0 || ^22.12.0 || >=24.0.0" - } - }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -11069,9 +10652,9 @@ } }, "node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", "dev": true, "license": "MIT", "engines": { @@ -11116,13 +10699,6 @@ "node": ">=12" } }, - "node_modules/xmlchars": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", - "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true, - "license": "MIT" - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", @@ -11141,9 +10717,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.8.4", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.4.tgz", - "integrity": "sha512-ml/JPOj9fOQK8RNnWojA67GbZ0ApXAUlN2UQclwv2eVgTgn7O9gg9o7paZWKMp4g0H3nTLtS9LVzhkpOFIKzog==", + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", "license": "ISC", "bin": { "yaml": "bin.mjs" diff --git a/frontend/package.json b/frontend/package.json index 23263de91..4975e565e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -8,7 +8,8 @@ }, "msw": { "workerDirectory": [ - ".storybook/public" + ".storybook/public", + "public" ] }, "scripts": { @@ -69,18 +70,17 @@ "@storybook/addon-a11y": "^10.3.6", "@storybook/vue3-vite": "^10.3.6", "@tailwindcss/vite": "^4.3.0", - "@testing-library/jest-dom": "^6.9.1", - "@testing-library/user-event": "^14.6.1", "@testing-library/vue": "^8.1.0", "@tsconfig/node24": "^24.0.4", "@types/js-yaml": "^4.0.9", - "@types/jsdom": "^28.0.1", "@types/lodash": "^4.17.24", "@types/luxon": "^3.7.1", "@types/node": "^24.12.3", "@types/pluralize": "^0.0.33", "@types/semver": "^7.7.1", "@vitejs/plugin-vue": "^6.0.6", + "@vitest/browser": "^4.1.6", + "@vitest/browser-playwright": "^4.1.6", "@vitest/eslint-plugin": "^1.6.17", "@vue/eslint-config-prettier": "^10.2.0", "@vue/eslint-config-typescript": "^14.7.0", @@ -92,11 +92,9 @@ "eslint-plugin-simple-import-sort": "^13.0.0", "eslint-plugin-storybook": "^10.3.6", "eslint-plugin-vue": "^10.9.1", - "fake-indexeddb": "^6.2.5", - "jsdom": "^29.1.1", "json-diff-ts": "^4.10.4", "kubernetes-types": "^1.30.0", - "msw": "^2.14.5", + "msw": "^2.14.6", "msw-storybook-addon": "^2.0.7", "openpgp": "^6.3.0", "prettier": "^3.8.3", @@ -108,10 +106,11 @@ "typescript": "^6.0.3", "vite": "^8.0.12", "vite-plugin-vue-devtools": "^8.1.2", - "vitest": "^4.1.5", + "vitest": "^4.1.6", + "vitest-browser-vue": "^2.1.0", "vue-component-type-helpers": "^3.2.8", "vue-tsc": "^3.2.8", - "yaml": "^2.8.4" + "yaml": "^2.9.0" }, "overrides": { "vue-router": "^5.0.6", diff --git a/frontend/public/mockServiceWorker.js b/frontend/public/mockServiceWorker.js new file mode 100644 index 000000000..33dde9e77 --- /dev/null +++ b/frontend/public/mockServiceWorker.js @@ -0,0 +1,349 @@ +/* eslint-disable */ +/* tslint:disable */ + +/** + * Mock Service Worker. + * @see https://github.com/mswjs/msw + * - Please do NOT modify this file. + */ + +const PACKAGE_VERSION = '2.14.6' +const INTEGRITY_CHECKSUM = '4db4a41e972cec1b64cc569c66952d82' +const IS_MOCKED_RESPONSE = Symbol('isMockedResponse') +const activeClientIds = new Set() + +addEventListener('install', function () { + self.skipWaiting() +}) + +addEventListener('activate', function (event) { + event.waitUntil(self.clients.claim()) +}) + +addEventListener('message', async function (event) { + const clientId = Reflect.get(event.source || {}, 'id') + + if (!clientId || !self.clients) { + return + } + + const client = await self.clients.get(clientId) + + if (!client) { + return + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + switch (event.data) { + case 'KEEPALIVE_REQUEST': { + sendToClient(client, { + type: 'KEEPALIVE_RESPONSE', + }) + break + } + + case 'INTEGRITY_CHECK_REQUEST': { + sendToClient(client, { + type: 'INTEGRITY_CHECK_RESPONSE', + payload: { + packageVersion: PACKAGE_VERSION, + checksum: INTEGRITY_CHECKSUM, + }, + }) + break + } + + case 'MOCK_ACTIVATE': { + activeClientIds.add(clientId) + + sendToClient(client, { + type: 'MOCKING_ENABLED', + payload: { + client: { + id: client.id, + frameType: client.frameType, + }, + }, + }) + break + } + + case 'CLIENT_CLOSED': { + activeClientIds.delete(clientId) + + const remainingClients = allClients.filter((client) => { + return client.id !== clientId + }) + + // Unregister itself when there are no more clients + if (remainingClients.length === 0) { + self.registration.unregister() + } + + break + } + } +}) + +addEventListener('fetch', function (event) { + const requestInterceptedAt = Date.now() + + // Bypass navigation requests. + if (event.request.mode === 'navigate') { + return + } + + // Opening the DevTools triggers the "only-if-cached" request + // that cannot be handled by the worker. Bypass such requests. + if ( + event.request.cache === 'only-if-cached' && + event.request.mode !== 'same-origin' + ) { + return + } + + // Bypass all requests when there are no active clients. + // Prevents the self-unregistered worked from handling requests + // after it's been terminated (still remains active until the next reload). + if (activeClientIds.size === 0) { + return + } + + const requestId = crypto.randomUUID() + event.respondWith(handleRequest(event, requestId, requestInterceptedAt)) +}) + +/** + * @param {FetchEvent} event + * @param {string} requestId + * @param {number} requestInterceptedAt + */ +async function handleRequest(event, requestId, requestInterceptedAt) { + const client = await resolveMainClient(event) + const requestCloneForEvents = event.request.clone() + const response = await getResponse( + event, + client, + requestId, + requestInterceptedAt, + ) + + // Send back the response clone for the "response:*" life-cycle events. + // Ensure MSW is active and ready to handle the message, otherwise + // this message will pend indefinitely. + if (client && activeClientIds.has(client.id)) { + const serializedRequest = await serializeRequest(requestCloneForEvents) + + // Clone the response so both the client and the library could consume it. + const responseClone = response.clone() + + sendToClient( + client, + { + type: 'RESPONSE', + payload: { + isMockedResponse: IS_MOCKED_RESPONSE in response, + request: { + id: requestId, + ...serializedRequest, + }, + response: { + type: responseClone.type, + status: responseClone.status, + statusText: responseClone.statusText, + headers: Object.fromEntries(responseClone.headers.entries()), + body: responseClone.body, + }, + }, + }, + responseClone.body ? [serializedRequest.body, responseClone.body] : [], + ) + } + + return response +} + +/** + * Resolve the main client for the given event. + * Client that issues a request doesn't necessarily equal the client + * that registered the worker. It's with the latter the worker should + * communicate with during the response resolving phase. + * @param {FetchEvent} event + * @returns {Promise} + */ +async function resolveMainClient(event) { + const client = await self.clients.get(event.clientId) + + if (activeClientIds.has(event.clientId)) { + return client + } + + if (client?.frameType === 'top-level') { + return client + } + + const allClients = await self.clients.matchAll({ + type: 'window', + }) + + return allClients + .filter((client) => { + // Get only those clients that are currently visible. + return client.visibilityState === 'visible' + }) + .find((client) => { + // Find the client ID that's recorded in the + // set of clients that have registered the worker. + return activeClientIds.has(client.id) + }) +} + +/** + * @param {FetchEvent} event + * @param {Client | undefined} client + * @param {string} requestId + * @param {number} requestInterceptedAt + * @returns {Promise} + */ +async function getResponse(event, client, requestId, requestInterceptedAt) { + // Clone the request because it might've been already used + // (i.e. its body has been read and sent to the client). + const requestClone = event.request.clone() + + function passthrough() { + // Cast the request headers to a new Headers instance + // so the headers can be manipulated with. + const headers = new Headers(requestClone.headers) + + // Remove the "accept" header value that marked this request as passthrough. + // This prevents request alteration and also keeps it compliant with the + // user-defined CORS policies. + const acceptHeader = headers.get('accept') + if (acceptHeader) { + const values = acceptHeader.split(',').map((value) => value.trim()) + const filteredValues = values.filter( + (value) => value !== 'msw/passthrough', + ) + + if (filteredValues.length > 0) { + headers.set('accept', filteredValues.join(', ')) + } else { + headers.delete('accept') + } + } + + return fetch(requestClone, { headers }) + } + + // Bypass mocking when the client is not active. + if (!client) { + return passthrough() + } + + // Bypass initial page load requests (i.e. static assets). + // The absence of the immediate/parent client in the map of the active clients + // means that MSW hasn't dispatched the "MOCK_ACTIVATE" event yet + // and is not ready to handle requests. + if (!activeClientIds.has(client.id)) { + return passthrough() + } + + // Notify the client that a request has been intercepted. + const serializedRequest = await serializeRequest(event.request) + const clientMessage = await sendToClient( + client, + { + type: 'REQUEST', + payload: { + id: requestId, + interceptedAt: requestInterceptedAt, + ...serializedRequest, + }, + }, + [serializedRequest.body], + ) + + switch (clientMessage.type) { + case 'MOCK_RESPONSE': { + return respondWithMock(clientMessage.data) + } + + case 'PASSTHROUGH': { + return passthrough() + } + } + + return passthrough() +} + +/** + * @param {Client} client + * @param {any} message + * @param {Array} transferrables + * @returns {Promise} + */ +function sendToClient(client, message, transferrables = []) { + return new Promise((resolve, reject) => { + const channel = new MessageChannel() + + channel.port1.onmessage = (event) => { + if (event.data && event.data.error) { + return reject(event.data.error) + } + + resolve(event.data) + } + + client.postMessage(message, [ + channel.port2, + ...transferrables.filter(Boolean), + ]) + }) +} + +/** + * @param {Response} response + * @returns {Response} + */ +function respondWithMock(response) { + // Setting response status code to 0 is a no-op. + // However, when responding with a "Response.error()", the produced Response + // instance will have status code set to 0. Since it's not possible to create + // a Response instance with status code 0, handle that use-case separately. + if (response.status === 0) { + return Response.error() + } + + const mockedResponse = new Response(response.body, response) + + Reflect.defineProperty(mockedResponse, IS_MOCKED_RESPONSE, { + value: true, + enumerable: true, + }) + + return mockedResponse +} + +/** + * @param {Request} request + */ +async function serializeRequest(request) { + return { + url: request.url, + mode: request.mode, + method: request.method, + headers: Object.fromEntries(request.headers.entries()), + cache: request.cache, + credentials: request.credentials, + destination: request.destination, + integrity: request.integrity, + redirect: request.redirect, + referrer: request.referrer, + referrerPolicy: request.referrerPolicy, + body: await request.arrayBuffer(), + keepalive: request.keepalive, + } +} diff --git a/frontend/src/api/watch.spec.ts b/frontend/src/api/watch.spec.ts index 4bf1ffa62..393d7587d 100644 --- a/frontend/src/api/watch.spec.ts +++ b/frontend/src/api/watch.spec.ts @@ -10,7 +10,8 @@ import { } from '@msw/helpers' import { createWatchStreamMock } from '@msw/server' import { waitFor } from '@testing-library/vue' -import { describe, expect, test } from 'vitest' +import { flushPromises } from '@vue/test-utils' +import { afterAll, beforeAll, describe, expect, test } from 'vitest' import { type Ref, ref } from 'vue' import { Runtime } from '@/api/common/omni.pb' @@ -19,7 +20,18 @@ import type { MachineSpec } from '@/api/omni/specs/omni.pb' import { DefaultNamespace, MachineType } from '@/api/resources' import Watch from '@/api/watch' +// Suppress "Failed to fetch" unhandled rejections that can occur when the +// stream retry timer fires after MSW handlers are cleared between tests. +const suppressFailedToFetch = (event: PromiseRejectionEvent) => { + if (event.reason instanceof TypeError && event.reason.message === 'Failed to fetch') { + event.preventDefault() + } +} + describe('watch', () => { + beforeAll(() => window.addEventListener('unhandledrejection', suppressFailedToFetch)) + afterAll(() => window.removeEventListener('unhandledrejection', suppressFailedToFetch)) + const items: Ref[]> = ref([]) const watch = new Watch(items) @@ -77,6 +89,8 @@ describe('watch', () => { watch.stop() expect(items.value).toHaveLength(0) + + await flushPromises() }) test('restarts handling', async () => { @@ -116,5 +130,7 @@ describe('watch', () => { }) watch.stop() + + await flushPromises() }) }) diff --git a/frontend/src/components/Button/SplitButton.spec.ts b/frontend/src/components/Button/SplitButton.spec.ts index f66f93829..e342a20f6 100644 --- a/frontend/src/components/Button/SplitButton.spec.ts +++ b/frontend/src/components/Button/SplitButton.spec.ts @@ -2,35 +2,34 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen } from '@testing-library/vue' import { expect, test, vi } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import SplitButton from './SplitButton.vue' test('sends click events', async () => { - const user = userEvent.setup() const clickFn = vi.fn() - render(SplitButton, { + const screen = await render(SplitButton, { props: { actions: ['one', 'two', 'three'], onClick: clickFn, }, }) - await user.click(screen.getByRole('button', { name: 'one' })) + await userEvent.click(screen.getByRole('button', { name: 'one' })) expect(clickFn).toHaveBeenCalledExactlyOnceWith('one') clickFn.mockClear() - await user.click(screen.getByRole('button', { name: 'extra actions' })) - await user.click(screen.getByRole('menuitem', { name: 'one' })) + await userEvent.click(screen.getByRole('button', { name: 'extra actions' })) + await userEvent.click(screen.getByRole('menuitem', { name: 'one' })) expect(clickFn).toHaveBeenCalledExactlyOnceWith('one') clickFn.mockClear() - await user.click(screen.getByRole('button', { name: 'extra actions' })) - await user.click(screen.getByRole('menuitem', { name: 'two' })) + await userEvent.click(screen.getByRole('button', { name: 'extra actions' })) + await userEvent.click(screen.getByRole('menuitem', { name: 'two' })) expect(clickFn).toHaveBeenCalledExactlyOnceWith('two') }) diff --git a/frontend/src/components/Modals/ConfirmModal.vue b/frontend/src/components/Modals/ConfirmModal.vue index 301755aa2..2e17c6a9a 100644 --- a/frontend/src/components/Modals/ConfirmModal.vue +++ b/frontend/src/components/Modals/ConfirmModal.vue @@ -53,6 +53,7 @@ const forwarded = useForwardPropsEmits(alertDialogRootProps, emit)
{{ title }} diff --git a/frontend/src/components/Modals/JoinTokenDelete.spec.ts b/frontend/src/components/Modals/JoinTokenDelete.spec.ts index 8a2305c86..7e3fa5b20 100644 --- a/frontend/src/components/Modals/JoinTokenDelete.spec.ts +++ b/frontend/src/components/Modals/JoinTokenDelete.spec.ts @@ -2,9 +2,10 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen, waitFor } from '@testing-library/vue' +import { waitFor } from '@testing-library/vue' import { beforeEach, describe, expect, test, vi } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import { defineComponent, onMounted } from 'vue' import { deleteJoinToken } from '@/methods/auth' @@ -41,7 +42,7 @@ describe('JoinTokenDelete', () => { beforeEach(() => vi.clearAllMocks()) test('renders the token in the title', async () => { - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -50,7 +51,7 @@ describe('JoinTokenDelete', () => { }) test('shows the permanent deletion warning', async () => { - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -61,7 +62,7 @@ describe('JoinTokenDelete', () => { }) test('action button is disabled before warnings are ready', async () => { - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: PendingWarningsStub } }, }) @@ -70,7 +71,7 @@ describe('JoinTokenDelete', () => { }) test('action button is enabled once warnings are ready', async () => { - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -82,7 +83,7 @@ describe('JoinTokenDelete', () => { const user = userEvent.setup() vi.mocked(deleteJoinToken).mockResolvedValue(undefined) - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -98,7 +99,7 @@ describe('JoinTokenDelete', () => { vi.mocked(deleteJoinToken).mockResolvedValue(undefined) const onUpdateOpen = vi.fn() - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true, 'onUpdate:open': onUpdateOpen }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -115,7 +116,7 @@ describe('JoinTokenDelete', () => { vi.mocked(deleteJoinToken).mockRejectedValue(new Error('Network error')) const onUpdateOpen = vi.fn() - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true, 'onUpdate:open': onUpdateOpen }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -132,7 +133,7 @@ describe('JoinTokenDelete', () => { test('closes the modal when Cancel is clicked without deleting', async () => { const user = userEvent.setup() - render(JoinTokenDelete, { + const screen = render(JoinTokenDelete, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) diff --git a/frontend/src/components/Modals/JoinTokenRevoke.spec.ts b/frontend/src/components/Modals/JoinTokenRevoke.spec.ts index a06659f32..58f9ccba1 100644 --- a/frontend/src/components/Modals/JoinTokenRevoke.spec.ts +++ b/frontend/src/components/Modals/JoinTokenRevoke.spec.ts @@ -2,9 +2,10 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen, waitFor } from '@testing-library/vue' +import { waitFor } from '@testing-library/vue' import { beforeEach, describe, expect, test, vi } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import { defineComponent, onMounted } from 'vue' import { revokeJoinToken } from '@/methods/auth' @@ -41,7 +42,7 @@ describe('JoinTokenRevoke', () => { beforeEach(() => vi.clearAllMocks()) test('renders the token in the title', async () => { - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -50,7 +51,7 @@ describe('JoinTokenRevoke', () => { }) test('shows the confirmation message', async () => { - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -59,7 +60,7 @@ describe('JoinTokenRevoke', () => { }) test('action button is disabled before warnings are ready', async () => { - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: PendingWarningsStub } }, }) @@ -68,7 +69,7 @@ describe('JoinTokenRevoke', () => { }) test('action button is enabled once warnings are ready', async () => { - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -80,7 +81,7 @@ describe('JoinTokenRevoke', () => { const user = userEvent.setup() vi.mocked(revokeJoinToken).mockResolvedValue(undefined) - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -96,7 +97,7 @@ describe('JoinTokenRevoke', () => { vi.mocked(revokeJoinToken).mockResolvedValue(undefined) const onUpdateOpen = vi.fn() - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true, 'onUpdate:open': onUpdateOpen }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -113,7 +114,7 @@ describe('JoinTokenRevoke', () => { vi.mocked(revokeJoinToken).mockRejectedValue(new Error('Permission denied')) const onUpdateOpen = vi.fn() - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true, 'onUpdate:open': onUpdateOpen }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) @@ -133,7 +134,7 @@ describe('JoinTokenRevoke', () => { test('closes the modal when Cancel is clicked without revoking', async () => { const user = userEvent.setup() - render(JoinTokenRevoke, { + const screen = render(JoinTokenRevoke, { props: { token: TOKEN, open: true }, global: { stubs: { JoinTokenWarnings: ReadyWarningsStub } }, }) diff --git a/frontend/src/components/Radio/RadioGroup.spec.ts b/frontend/src/components/Radio/RadioGroup.spec.ts index 2f41b5424..8e0c5f94a 100644 --- a/frontend/src/components/Radio/RadioGroup.spec.ts +++ b/frontend/src/components/Radio/RadioGroup.spec.ts @@ -2,9 +2,9 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen, waitFor } from '@testing-library/vue' import { expect, test } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import RadioGroup from './RadioGroup.vue' import RadioGroupOption from './RadioGroupOption.vue' @@ -18,9 +18,7 @@ const options = Array(5) })) test('allows selection', async () => { - const user = userEvent.setup() - - render(RadioGroup, { + const screen = await render(RadioGroup, { props: { label: 'My radio', }, @@ -44,11 +42,9 @@ test('allows selection', async () => { }, }) - await waitFor(() => { - expect(screen.getByRole('radio', { name: options[0].label })).not.toBeChecked() - }) - - await user.click(screen.getByRole('radio', { name: options[0].label })) + const radio = screen.getByRole('radio', { name: options[0].label }) - expect(screen.getByRole('radio', { name: options[0].label })).toBeChecked() + await expect.element(radio).not.toBeChecked() + await userEvent.click(radio) + await expect.element(radio).toBeChecked() }) diff --git a/frontend/src/components/SelectList/TSelectList.spec.ts b/frontend/src/components/SelectList/TSelectList.spec.ts index e63cd8b76..a763ff08a 100644 --- a/frontend/src/components/SelectList/TSelectList.spec.ts +++ b/frontend/src/components/SelectList/TSelectList.spec.ts @@ -2,28 +2,28 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen, waitFor } from '@testing-library/vue' import { enableAutoUnmount, mount } from '@vue/test-utils' import { afterEach, expect, test, vi } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import TSelectList from './TSelectList.vue' enableAutoUnmount(afterEach) -test('is accessible with inline label', () => { - render(TSelectList, { +test('is accessible with inline label', async () => { + const screen = await render(TSelectList, { props: { title: 'My select', values: ['first option', 'second option'], }, }) - expect(screen.getByLabelText('My select')).toBeInTheDocument() + await expect.element(screen.getByLabelText('My select')).toBeInTheDocument() }) -test('is accessible with overhead label', () => { - render(TSelectList, { +test('is accessible with overhead label', async () => { + const screen = await render(TSelectList, { props: { title: 'My select', values: ['first option', 'second option'], @@ -31,15 +31,14 @@ test('is accessible with overhead label', () => { }, }) - expect(screen.getByLabelText('My select')).toBeInTheDocument() + await expect.element(screen.getByLabelText('My select')).toBeInTheDocument() }) test('accepts a default value', async () => { - const user = userEvent.setup() const updateFn = vi.fn() const checkedFn = vi.fn() - render(TSelectList, { + const screen = await render(TSelectList, { props: { title: 'My select', values: ['first option', 'second option'], @@ -54,23 +53,21 @@ test('accepts a default value', async () => { expect(updateFn).toHaveBeenCalledExactlyOnceWith('first option') expect(checkedFn).not.toHaveBeenCalled() - expect(trigger).toHaveTextContent('first option') + await expect.element(trigger).toHaveTextContent('first option') // Open dropdown - await user.click(trigger) + await userEvent.click(trigger) - expect(screen.getByRole('option', { name: 'first option' })).toHaveAttribute( - 'aria-selected', - 'true', - ) + await expect + .element(screen.getByRole('option', { name: 'first option' })) + .toHaveAttribute('aria-selected', 'true') }) test('allows selection', async () => { - const user = userEvent.setup() const updateFn = vi.fn() const checkedFn = vi.fn() - render(TSelectList, { + const screen = await render(TSelectList, { props: { title: 'My select', values: ['first option', 'second option'], @@ -81,27 +78,28 @@ test('allows selection', async () => { const trigger = screen.getByLabelText('My select') - expect(trigger.textContent).toBe('My select') // Exact match to assert no default + await expect.element(trigger).toHaveTextContent('My select') // Open dropdown - await user.click(trigger) + await userEvent.click(trigger) const option = screen.getByRole('option', { name: 'second option' }) // Select option - await user.click(option) + await userEvent.click(option) expect(updateFn).toHaveBeenCalledExactlyOnceWith('second option') expect(checkedFn).toHaveBeenCalledExactlyOnceWith('second option') - expect(trigger).toHaveTextContent('second option') - expect(option).toHaveAttribute('aria-selected', 'true') + await expect.element(trigger).toHaveTextContent('second option') + // Cannot check aria-selected after selection — Reka UI closes the dropdown and removes options from the DOM }) test('exposes selectItem', async () => { const updateFn = vi.fn() const checkedFn = vi.fn() + // FIXME: maybe now is possibru // Can't test defineExpose with testing-library, using @vue/test-utils instead const wrapper = mount(TSelectList, { props: { @@ -119,15 +117,12 @@ test('exposes selectItem', async () => { expect(updateFn).toHaveBeenCalledExactlyOnceWith('second option') expect(checkedFn).toHaveBeenCalledExactlyOnceWith('second option') - await waitFor(() => { - expect(wrapper.text()).toContain('second option') - }) + await wrapper.vm.$nextTick() + expect(wrapper.text()).toContain('second option') }) test('focuses search on open', async () => { - const user = userEvent.setup() - - render(TSelectList, { + const screen = await render(TSelectList, { props: { title: 'My select', values: ['first option', 'second option'], @@ -138,7 +133,7 @@ test('focuses search on open', async () => { const trigger = screen.getByLabelText('My select') // Open dropdown - await user.click(trigger) + await userEvent.click(trigger) - expect(screen.getByRole('textbox', { name: 'search' })).toHaveFocus() + await expect.element(screen.getByRole('textbox', { name: 'search' })).toHaveFocus() }) diff --git a/frontend/src/components/TInput/TInput.spec.ts b/frontend/src/components/TInput/TInput.spec.ts index 85d794abd..20fdf37fa 100644 --- a/frontend/src/components/TInput/TInput.spec.ts +++ b/frontend/src/components/TInput/TInput.spec.ts @@ -2,25 +2,25 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import userEvent from '@testing-library/user-event' -import { render, screen } from '@testing-library/vue' import { expect, test } from 'vitest' +import { userEvent } from 'vitest/browser' +import { render } from 'vitest-browser-vue' import TInput from './TInput.vue' -test('is accessible with inline label', () => { - render(TInput, { +test('is accessible with inline label', async () => { + const screen = await render(TInput, { props: { modelValue: '', title: 'My input', }, }) - expect(screen.getByLabelText('My input')).toBeInTheDocument() + await expect.element(screen.getByLabelText('My input')).toBeInTheDocument() }) -test('is accessible with overhead label', () => { - render(TInput, { +test('is accessible with overhead label', async () => { + const screen = await render(TInput, { props: { modelValue: '', title: 'My input', @@ -28,61 +28,57 @@ test('is accessible with overhead label', () => { }, }) - expect(screen.getByLabelText('My input')).toBeInTheDocument() + await expect.element(screen.getByLabelText('My input')).toBeInTheDocument() }) test('allows input', async () => { - const user = userEvent.setup() - - render(TInput, { + const screen = await render(TInput, { props: { modelValue: 'hello', title: 'My input', }, }) - expect(screen.getByLabelText('My input')).toHaveValue('hello') + await expect.element(screen.getByLabelText('My input')).toHaveValue('hello') - await user.type(screen.getByLabelText('My input'), 'potatoes') + await userEvent.type(screen.getByLabelText('My input'), 'potatoes') - expect(screen.getByLabelText('My input')).toHaveValue('hellopotatoes') + await expect.element(screen.getByLabelText('My input')).toHaveValue('hellopotatoes') }) test('is clearable', async () => { - const user = userEvent.setup() - - render(TInput, { + const screen = await render(TInput, { props: { modelValue: 'hello', title: 'My input', }, }) - expect(screen.getByLabelText('My input')).toHaveValue('hello') + await expect.element(screen.getByLabelText('My input')).toHaveValue('hello') - await user.click(screen.getByRole('button', { name: 'clear' })) + await userEvent.click(screen.getByRole('button', { name: 'clear' })) - expect(screen.getByLabelText('My input')).toHaveValue('') + await expect.element(screen.getByLabelText('My input')).toHaveValue('') }) test('is focusable', async () => { - const { rerender } = render(TInput, { + const screen = await render(TInput, { props: { modelValue: '', title: 'My input', }, }) - expect(screen.getByLabelText('My input')).not.toHaveFocus() + await expect.element(screen.getByLabelText('My input')).not.toHaveFocus() - // due to jsdom limitations trying to test for component being initially focused fails - await rerender({ focus: true }) + // trying to test for component being initially focused fails, rerender instead + await screen.rerender({ focus: true }) - expect(screen.getByLabelText('My input')).toHaveFocus() + await expect.element(screen.getByLabelText('My input')).toHaveFocus() }) test('is disableable', async () => { - const { rerender } = render(TInput, { + const screen = await render(TInput, { props: { modelValue: '', title: 'My input', @@ -90,9 +86,9 @@ test('is disableable', async () => { }, }) - expect(screen.getByLabelText('My input')).toBeDisabled() + await expect.element(screen.getByLabelText('My input')).toBeDisabled() - await rerender({ disabled: false }) + await screen.rerender({ disabled: false }) - expect(screen.getByLabelText('My input')).not.toBeDisabled() + await expect.element(screen.getByLabelText('My input')).not.toBeDisabled() }) diff --git a/frontend/src/methods/auth.spec.ts b/frontend/src/methods/auth.spec.ts index f9ad28e8e..804f72558 100644 --- a/frontend/src/methods/auth.spec.ts +++ b/frontend/src/methods/auth.spec.ts @@ -3,7 +3,7 @@ // Use of this software is governed by the Business Source License // included in the LICENSE file. import { useAuth0 } from '@auth0/auth0-vue' -import { server } from '@msw/server' +import { worker } from '@msw/server' import { waitFor } from '@testing-library/vue' import { http, HttpResponse } from 'msw' import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest' @@ -18,6 +18,7 @@ import { AuthType, authType } from '@/methods' import { useClusterPermissions, useLogout } from '@/methods/auth' import { useIdentity } from '@/methods/identity' import { useKeys } from '@/methods/key' +import { redirectToURL } from '@/methods/navigate' vi.mock('@auth0/auth0-vue', () => ({ useAuth0: vi.fn(), @@ -30,6 +31,9 @@ vi.mock('@/api/omni/auth/auth.pb', () => ({ RevokePublicKey: vi.fn(), }, })) +vi.mock('@/methods/navigate', () => ({ + redirectToURL: vi.fn(), +})) describe('useLogout', () => { let mockKeys: ReturnType @@ -37,8 +41,6 @@ describe('useLogout', () => { let mockAuth0: { logout: ReturnType } - let originalLocation: Location - let mockLocation: Location beforeEach(() => { vi.clearAllMocks() @@ -65,33 +67,10 @@ describe('useLogout', () => { logout: vi.fn().mockResolvedValue(undefined), } vi.mocked(useAuth0).mockReturnValue(mockAuth0 as unknown as ReturnType) - - originalLocation = window.location - mockLocation = { - ...originalLocation, - href: 'http://localhost:3000', - origin: 'http://localhost:3000', - } as Location - delete (window as { location?: Location }).location - Object.defineProperty(window, 'location', { - value: mockLocation, - writable: true, - configurable: true, - }) - - Object.defineProperty(window, 'top', { - value: window, - writable: true, - configurable: true, - }) }) afterEach(() => { - Object.defineProperty(window, 'location', { - value: originalLocation, - writable: true, - configurable: true, - }) + vi.restoreAllMocks() vi.clearAllMocks() }) @@ -156,7 +135,7 @@ describe('useLogout', () => { expect(mockAuth0.logout).toHaveBeenCalledWith({ logoutParams: { - returnTo: 'http://localhost:3000', + returnTo: window.location.origin, }, }) expect(mockKeys.clear).toHaveBeenCalled() @@ -175,7 +154,7 @@ describe('useLogout', () => { await logout() expect(mockAuth0.logout).not.toHaveBeenCalled() - expect(window.location.href).toBe('/logout?flow=frontend') + expect(vi.mocked(redirectToURL)).toHaveBeenCalledWith('/logout?flow=frontend') expect(mockKeys.clear).toHaveBeenCalled() expect(mockIdentity.clear).toHaveBeenCalled() }, @@ -204,7 +183,7 @@ describe('useClusterPermissions', () => { }) test('reflects loaded spec fields', async () => { - server.use(makeHandler({ can_update_kubernetes: true, can_update_talos: false })) + worker.use(makeHandler({ can_update_kubernetes: true, can_update_talos: false })) const { canUpdateKubernetes, canUpdateTalos } = useClusterPermissions('perm-fields') await waitFor(() => expect(canUpdateKubernetes.value).toBe(true)) expect(canUpdateTalos.value).toBe(false) @@ -223,7 +202,7 @@ describe('useClusterPermissions', () => { }), }), ) - server.use(http.post('/omni.resources.ResourceService/Get', handler)) + worker.use(http.post('/omni.resources.ResourceService/Get', handler)) const { canUpdateKubernetes: a } = useClusterPermissions('perm-cached') const { canUpdateKubernetes: b } = useClusterPermissions('perm-cached') @@ -240,7 +219,7 @@ describe('useClusterPermissions', () => { } // Single handler — two chained handlers can't both read the request body stream. - server.use( + worker.use( http.post('/omni.resources.ResourceService/Get', async ({ request }) => { const body = (await request.json()) as { id: string; type: string } if (body.type !== ClusterPermissionsType) return diff --git a/frontend/src/methods/auth.ts b/frontend/src/methods/auth.ts index a0fe47f3a..fc7ef6b7a 100644 --- a/frontend/src/methods/auth.ts +++ b/frontend/src/methods/auth.ts @@ -34,6 +34,7 @@ import { import { AuthType, authType } from '@/methods' import { useIdentity } from '@/methods/identity' import { useKeys } from '@/methods/key' +import { redirectToURL } from '@/methods/navigate' import { useResourceGet } from '@/methods/useResourceGet' const authScope = effectScope(true) @@ -181,14 +182,6 @@ const updateToken = async (tokenID: string, update: (token: Resource { - if (window.top) { - window.top.location.href = url - } else { - window.location.href = url - } -} - export function useLogout() { const auth0 = authType.value === AuthType.Auth0 ? useAuth0() : null const keys = useKeys() diff --git a/frontend/src/methods/cluster.spec.ts b/frontend/src/methods/cluster.spec.ts index a0324c3ef..0765a1656 100644 --- a/frontend/src/methods/cluster.spec.ts +++ b/frontend/src/methods/cluster.spec.ts @@ -19,6 +19,8 @@ vi.mock('@/api/grpc', () => ({ vi.mock('@/api/options', () => ({ withRuntime: vi.fn((runtime: Runtime) => ({ runtime })), + withSelectors: vi.fn(), + withTimeout: vi.fn(), })) describe('nextAvailableClusterName', () => { diff --git a/frontend/src/methods/index.spec.ts b/frontend/src/methods/index.spec.ts index 5d707d283..96a728980 100644 --- a/frontend/src/methods/index.spec.ts +++ b/frontend/src/methods/index.spec.ts @@ -2,9 +2,8 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import { expect } from '@playwright/test' import { parse } from 'semver' -import { test } from 'vitest' +import { expect, test } from 'vitest' import { DefaultTalosVersion } from '@/api/resources' diff --git a/frontend/src/methods/key.spec.ts b/frontend/src/methods/key.spec.ts index a9098f3d8..95d48c38b 100644 --- a/frontend/src/methods/key.spec.ts +++ b/frontend/src/methods/key.spec.ts @@ -2,7 +2,7 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import { server } from '@msw/server' +import { worker } from '@msw/server' import { enableAutoUnmount, flushPromises, mount } from '@vue/test-utils' import { add, isAfter, isBefore, milliseconds, sub } from 'date-fns' import { http, HttpResponse } from 'msw' @@ -22,7 +22,9 @@ beforeEach(() => { vi.mocked(useRoute, { partial: true }).mockReturnValue({}) vi.mocked(useRouter, { partial: true }).mockReturnValue({ isReady: vi.fn(), - resolve: vi.fn().mockReturnValue({ href: '' }), + // Return '#' so that window.location.replace('#') does a hash navigation + // instead of a full reload, preventing the test iframe from restarting. + resolve: vi.fn().mockReturnValue({ href: '#' }), }) vi.mocked(useRouter().isReady).mockImplementation(async () => { @@ -272,7 +274,7 @@ describe('createKeys', () => { test('creates & registers keys with the api', async () => { const emailRef = { email: '' } - server.use( + worker.use( http.post( '/auth.AuthService/RegisterPublicKey', async ({ request }) => { diff --git a/frontend/src/methods/navigate.ts b/frontend/src/methods/navigate.ts new file mode 100644 index 000000000..c2668f278 --- /dev/null +++ b/frontend/src/methods/navigate.ts @@ -0,0 +1,19 @@ +// Copyright (c) 2026 Sidero Labs, Inc. +// +// Use of this software is governed by the Business Source License +// included in the LICENSE file. + +/** + * Redirect the user's top-most window to the given URL. + * + * This makes sure that the redirect works correctly when the call comes from inside an iframe. + * + * @param url The URL to redirect to. + */ +export function redirectToURL(url: string) { + if (window.top) { + window.top.location.href = url + } else { + window.location.href = url + } +} diff --git a/frontend/src/methods/useResourceGet.spec.ts b/frontend/src/methods/useResourceGet.spec.ts index 788bcdfaf..51f3f778c 100644 --- a/frontend/src/methods/useResourceGet.spec.ts +++ b/frontend/src/methods/useResourceGet.spec.ts @@ -2,7 +2,7 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import { server } from '@msw/server' +import { worker } from '@msw/server' import { waitFor } from '@testing-library/vue' import { http, HttpResponse } from 'msw' import { describe, expect, test, vi } from 'vitest' @@ -19,7 +19,7 @@ describe('useResourceGet', () => { spec: { foo: 'bar' }, } - server.use( + worker.use( http.post('/omni.resources.ResourceService/Get', async ({ request }) => { const body = await request.json() expect(body).toEqual({ @@ -50,7 +50,7 @@ describe('useResourceGet', () => { const handler = vi.fn(() => HttpResponse.json({ body: JSON.stringify(resource) })) - server.use(http.post('/omni.resources.ResourceService/Get', handler)) + worker.use(http.post('/omni.resources.ResourceService/Get', handler)) const { data, loading, loadData } = useResourceGet({ runtime: Runtime.Omni, diff --git a/frontend/src/methods/useResourceList.spec.ts b/frontend/src/methods/useResourceList.spec.ts index 55c4ccc9d..d09c119df 100644 --- a/frontend/src/methods/useResourceList.spec.ts +++ b/frontend/src/methods/useResourceList.spec.ts @@ -2,7 +2,7 @@ // // Use of this software is governed by the Business Source License // included in the LICENSE file. -import { server } from '@msw/server' +import { worker } from '@msw/server' import { waitFor } from '@testing-library/vue' import { http, HttpResponse } from 'msw' import { describe, expect, test, vi } from 'vitest' @@ -25,7 +25,7 @@ describe('useResourceList', () => { }, ] - server.use( + worker.use( http.post('/omni.resources.ResourceService/List', async ({ request }) => { const body = await request.json() expect(body).toEqual({ @@ -65,7 +65,7 @@ describe('useResourceList', () => { }), ) - server.use(http.post('/omni.resources.ResourceService/List', handler)) + worker.use(http.post('/omni.resources.ResourceService/List', handler)) const { data, loading, loadData } = useResourceList({ runtime: Runtime.Omni, diff --git a/frontend/src/methods/useResourceWatch.spec.ts b/frontend/src/methods/useResourceWatch.spec.ts index 68775a6cf..8e80efed8 100644 --- a/frontend/src/methods/useResourceWatch.spec.ts +++ b/frontend/src/methods/useResourceWatch.spec.ts @@ -4,15 +4,16 @@ // included in the LICENSE file. import { createBootstrapEvent, createCreatedEvent } from '@msw/helpers' import { createWatchStreamMock } from '@msw/server' -import { render, waitFor } from '@testing-library/vue' +import { waitFor } from '@testing-library/vue' import { describe, expect, test } from 'vitest' +import { render } from 'vitest-browser-vue' import { defineComponent, nextTick, ref } from 'vue' import { Runtime } from '@/api/common/omni.pb' import { useResourceWatch } from './useResourceWatch' -function renderComposable(factory: () => T) { +async function renderComposable(factory: () => T) { let composableResult: T const TestComponent = defineComponent({ @@ -22,7 +23,7 @@ function renderComposable(factory: () => T) { template: '