From 6005765723abf782c144d02a2ed3d57abcd40e62 Mon Sep 17 00:00:00 2001 From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:07:33 +0530 Subject: [PATCH 1/7] UN-4329 [FEAT] Add the v1 unit-test harness and the frontend-v1-unit rig group - vitest.v1.config.mts runs src/v1 (minus plugins/) with its own setup, coverage report (coverage/v1) and 80% threshold; vitest.v1-plugins.config.mts runs cloud v1 plugins. vitest.config.mjs now excludes src/v1. - src/v1/test: renderV1/renderHookV1 over one pluggable provider chain (QueryClient, MemoryRouter, v1 theme root + #v1-portal), typed session and distribution, MSW at the HTTP boundary with a catch-all that fails the test naming every unmocked request, axe-core assertions, observer/matchMedia fakes. - Rig: optional vitest_config on a group (vitest runner only, must exist inside workdir), passed as --config; existing groups' commands are unchanged. - test-fixtures/v1-harness proves an unmocked request fails a real vitest run. - Deps (exact): msw 2.15.0, axe-core 4.14.0, @vitest/coverage-v8 3.2.6, @tanstack/react-query 5.104.5. --- frontend/bun.lock | 216 +++++++++++- frontend/package.json | 6 +- frontend/src/v1/test/a11y.ts | 89 +++++ frontend/src/v1/test/dom.ts | 209 ++++++++++++ frontend/src/v1/test/harness.test.tsx | 311 ++++++++++++++++++ frontend/src/v1/test/index.ts | 52 +++ frontend/src/v1/test/network.ts | 51 +++ frontend/src/v1/test/providers.tsx | 113 +++++++ frontend/src/v1/test/render.tsx | 100 ++++++ frontend/src/v1/test/session.ts | 51 +++ frontend/src/v1/test/setup.ts | 40 +++ .../src/v1/components/ui/Label.test.ts | 2 +- .../src/v1/pages/projects/title.test.ts | 2 +- .../src/v1/test/{render.ts => case-helper.ts} | 0 .../src/v1/components/ui/Helper.ts | 2 +- .../src/v1/test/{render.ts => case-helper.ts} | 0 .../v1-harness/cases/mocked-fetch.test.ts | 16 + .../v1-harness/cases/unmocked-fetch.test.tsx | 21 ++ .../v1-harness/cases/unmocked-xhr.test.ts | 13 + .../v1-harness/harness.fixtures.mjs | 58 ++++ .../v1-harness/vitest.config.mts | 19 ++ frontend/vitest.config.mjs | 4 + frontend/vitest.v1-plugins.config.mts | 4 + frontend/vitest.v1.config.mts | 99 ++++++ tests/groups.yaml | 16 + tests/rig/cli.py | 2 + tests/rig/groups.py | 36 ++ tests/rig/tests/conftest.py | 18 + tests/rig/tests/test_cli.py | 28 ++ tests/rig/tests/test_groups.py | 69 ++++ 30 files changed, 1630 insertions(+), 17 deletions(-) create mode 100644 frontend/src/v1/test/a11y.ts create mode 100644 frontend/src/v1/test/dom.ts create mode 100644 frontend/src/v1/test/harness.test.tsx create mode 100644 frontend/src/v1/test/index.ts create mode 100644 frontend/src/v1/test/network.ts create mode 100644 frontend/src/v1/test/providers.tsx create mode 100644 frontend/src/v1/test/render.tsx create mode 100644 frontend/src/v1/test/session.ts create mode 100644 frontend/src/v1/test/setup.ts rename frontend/test-fixtures/v1-boundary/cases/v1-allowed-imports/src/v1/test/{render.ts => case-helper.ts} (100%) rename frontend/test-fixtures/v1-boundary/cases/v1-non-test-imports-test/src/v1/test/{render.ts => case-helper.ts} (100%) create mode 100644 frontend/test-fixtures/v1-harness/cases/mocked-fetch.test.ts create mode 100644 frontend/test-fixtures/v1-harness/cases/unmocked-fetch.test.tsx create mode 100644 frontend/test-fixtures/v1-harness/cases/unmocked-xhr.test.ts create mode 100644 frontend/test-fixtures/v1-harness/harness.fixtures.mjs create mode 100644 frontend/test-fixtures/v1-harness/vitest.config.mts create mode 100644 frontend/vitest.v1-plugins.config.mts create mode 100644 frontend/vitest.v1.config.mts diff --git a/frontend/bun.lock b/frontend/bun.lock index 94f3b30008..e9074af073 100644 --- a/frontend/bun.lock +++ b/frontend/bun.lock @@ -18,6 +18,7 @@ "@rjsf/utils": "^5.8.1", "@rjsf/validator-ajv8": "^5.8.1", "@stripe/stripe-js": "^4.2.0", + "@tanstack/react-query": "5.104.5", "@tanstack/react-table": "^8.21.3", "axios": "1.16.0", "class-variance-authority": "^0.7.1", @@ -78,9 +79,12 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.4.0", + "@vitest/coverage-v8": "3.2.6", "autoprefixer": "^10.5.4", + "axe-core": "4.14.0", "baseline-browser-mapping": "^2.9.19", "happy-dom": "^20.10.2", + "msw": "2.15.0", "postcss": "^8.5.23", "postcss-value-parser": "^4.2.0", "tailwindcss": "^4", @@ -103,6 +107,8 @@ "@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, "sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw=="], + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + "@babel/code-frame": ["@babel/code-frame@7.28.6", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-JYgintcMjRiCvS8mMECzaEn+m3PfoQiyqukOMCCVQtoJGYJw8j/8LBJEiqkHLkfwCcs74E3pbAUFNg7d9VNJ+Q=="], "@babel/compat-data": ["@babel/compat-data@7.28.6", "", {}, "sha512-2lfu57JtzctfIrcGMz992hyLlByuzgIk58+hhGCxjKZ3rWI82NnVLjXcaTqkI2NvlcvOskZaiZ5kjUALo3Lpxg=="], @@ -143,6 +149,8 @@ "@babel/types": ["@babel/types@7.28.6", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-0ZrskXVEHSWIqZM/sQZ4EV3jZJXRkio/WCxaqKZP1g//CEWEPSfeZFcms4XeKBCHU0ZKnIkdJeU/kF+eRp5lBg=="], + "@bcoe/v8-coverage": ["@bcoe/v8-coverage@1.0.2", "", {}, "sha512-6zABk/ECA/QYSCQ1NGiVwwbQerUCZ+TQbp64Q3AgmfNvurHH0j8TtXa1qbShXA6qqkpAj4V5W8pP6mLe1mcMqA=="], + "@biomejs/biome": ["@biomejs/biome@2.3.13", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.3.13", "@biomejs/cli-darwin-x64": "2.3.13", "@biomejs/cli-linux-arm64": "2.3.13", "@biomejs/cli-linux-arm64-musl": "2.3.13", "@biomejs/cli-linux-x64": "2.3.13", "@biomejs/cli-linux-x64-musl": "2.3.13", "@biomejs/cli-win32-arm64": "2.3.13", "@biomejs/cli-win32-x64": "2.3.13" }, "bin": { "biome": "bin/biome" } }, "sha512-Fw7UsV0UAtWIBIm0M7g5CRerpu1eKyKAXIazzxhbXYUyMkwNrkX/KLkGI7b+uVDQ5cLUMfOC9vR60q9IDYDstA=="], "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.3.13", "", { "os": "darwin", "cpu": "arm64" }, "sha512-0OCwP0/BoKzyJHnFdaTk/i7hIP9JHH9oJJq6hrSCPmJPo8JWcJhprK4gQlhFzrwdTBAW4Bjt/RmCf3ZZe59gwQ=="], @@ -251,6 +259,20 @@ "@fontsource/inter": ["@fontsource/inter@5.3.0", "", {}, "sha512-RofMylZmjlJEfELXeNHFWBRcSs75rGU/6bV2S2jfnvv/3rPXPGe0LgUJTklcHZ9lM4OZmAVFhcJPnACfb91A3g=="], + "@inquirer/ansi": ["@inquirer/ansi@2.0.10", "", {}, "sha512-dwrc+1aqVZpHNkNDswXGtn1typ+Pxpo5FHZRyBw1le7onEJUkPUkYOY1FIFC1LvFMYUqqLuWBwElhAmencL7MQ=="], + + "@inquirer/confirm": ["@inquirer/confirm@6.3.5", "", { "dependencies": { "@inquirer/core": "^12.0.6", "@inquirer/type": "4.2.1" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BJ59BOyURiZiAxdr9AuRu38/k+rV+DcTIM2fUF/c/BXrab6H8zMZTTOl3LgkSJH96Xob2vOJxDiBkHiz32Xx2w=="], + + "@inquirer/core": ["@inquirer/core@12.0.6", "", { "dependencies": { "@inquirer/ansi": "^2.0.10", "@inquirer/figures": "^2.0.10", "@inquirer/type": "4.2.1", "cli-width": "^4.1.0", "fast-wrap-ansi": "^0.2.0", "mute-stream": "^3.0.0", "signal-exit": "^4.1.0" }, "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-5yxn2LsJ8Tu4ei2a+yNkmFRhM/cb+JMEsEs/AW6CCOnBnpMy4IjYiPMFyHtYq8weOmlTyqqCx40y0DsD7OsEPw=="], + + "@inquirer/figures": ["@inquirer/figures@2.0.10", "", {}, "sha512-51elrUJV3jZpvD0ygz8+U10Kc6+Lzqln+/tYmnbhy6jo5oXpE6hMCcv+OTBC0enwJXsb3JIuT3VtEinRNCSK2A=="], + + "@inquirer/type": ["@inquirer/type@4.2.1", "", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-lDkPsnv5M5CcYB2T1qTc3dobNTPkVr4DU3Ic2Q6SW8/qVRHgvzdRdmVZ6YqG9Ag1aJTeMsLU0s0WPRh3CZWDQQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="], + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], "@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="], @@ -267,6 +289,14 @@ "@monaco-editor/react": ["@monaco-editor/react@4.7.0", "", { "dependencies": { "@monaco-editor/loader": "^1.5.0" }, "peerDependencies": { "monaco-editor": ">= 0.25.0 < 1", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-cyzXQCtO47ydzxpQtCGSQGOC8Gk3ZUeBXFAxD+CWXYFo5OqZyZUonFl0DwUlTyAfRHntBfw2p3w4s9R6oe1eCA=="], + "@mswjs/interceptors": ["@mswjs/interceptors@0.41.9", "", { "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" } }, "sha512-VVPPgHyQ6ShqnrmDWuxjmUIsO9gWyOZFmuOfLd9LfBGQJwZfy0gvv9pbHSJuoFNIYC7ZDX9aoFwowjcdSC4E8w=="], + + "@open-draft/deferred-promise": ["@open-draft/deferred-promise@3.0.0", "", {}, "sha512-XW375UK8/9SqUVNVa6M0yEy8+iTi4QN5VZ7aZuRFQmy76LRwI9wy5F4YIBU6T+eTe2/DNDo8tqu8RHlwLHM6RA=="], + + "@open-draft/logger": ["@open-draft/logger@0.3.0", "", { "dependencies": { "is-node-process": "^1.2.0", "outvariant": "^1.4.0" } }, "sha512-X2g45fzhxH238HKO4xbSr7+wBS8Fvw6ixhTDuvLd5mqh6bJJCFAPwU9mPDxbcrRtfxv4u5IHCEH77BmxvXmmxQ=="], + + "@open-draft/until": ["@open-draft/until@2.1.0", "", {}, "sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg=="], + "@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="], "@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.208.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-CjruKY9V6NMssL/T1kAFgzosF1v9o6oeN+aX5JB/C/xPNtmgIJqcXHG7fA82Ou1zCpWGl4lROQUKwUNE1pMCyg=="], @@ -289,6 +319,8 @@ "@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.39.0", "", {}, "sha512-R5R9tb2AXs2IRLNKLBJDynhkfmx7mX0vi8NkhZb3gUkPWHn6HXk5J8iQ/dql0U3ApfWym4kXXmBDRGO+oeOfjg=="], + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + "@posthog/core": ["@posthog/core@1.15.0", "", { "dependencies": { "cross-spawn": "^7.0.6" } }, "sha512-n2/Yy0+qc8xhmlcOFiYqTcGHBZuuaQjVolfFXk7yTCynzdMe8Fx1zYvPPUrbdQK5tWwXyilkzybpqhK6I7aV4Q=="], "@posthog/types": ["@posthog/types@1.336.1", "", {}, "sha512-KSGst/a/HK7GhfLSbwAy35HtU3KjDqjLtq3+PoDlGfbz9SbO0owjc6jo6hAHnMz67QTSvrn/r0xgimDO4NQ+rA=="], @@ -611,6 +643,10 @@ "@tailwindcss/vite": ["@tailwindcss/vite@4.3.3", "", { "dependencies": { "@tailwindcss/node": "4.3.3", "@tailwindcss/oxide": "4.3.3", "tailwindcss": "4.3.3" }, "peerDependencies": { "vite": "^5.2.0 || ^6 || ^7 || ^8" } }, "sha512-yYU8cogLeSh/ms2jh8Fj7jaba/EWa7Ja6GoUqYZaraEuCI5YS6ms6ObZgjjedm+jm6XZjdNRWBpPP6Z86oOxcw=="], + "@tanstack/query-core": ["@tanstack/query-core@5.104.5", "", {}, "sha512-Q94gI8Opd+CbdVNOdj8ZCyV8WqLYitrDq1t96Xc8WZT6lY0r5iRnJLXYDxMex7EEf2v+mXvn4FcjaBKSxXZQ9A=="], + + "@tanstack/react-query": ["@tanstack/react-query@5.104.5", "", { "dependencies": { "@tanstack/query-core": "5.104.5" }, "peerDependencies": { "@types/react": "^18 || ^19", "react": "^18 || ^19" }, "optionalPeers": ["@types/react"] }, "sha512-RORgFe052U+93weaNuqg7qHq7vNVNzXN/VrXqD4bbAuQCWUqOl2lV5Ui9flWk5CVvMyL/Bh27QWhzwH0aaOx9g=="], + "@tanstack/react-table": ["@tanstack/react-table@8.21.3", "", { "dependencies": { "@tanstack/table-core": "8.21.3" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww=="], "@tanstack/table-core": ["@tanstack/table-core@8.21.3", "", {}, "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg=="], @@ -679,6 +715,10 @@ "@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ=="], + "@types/set-cookie-parser": ["@types/set-cookie-parser@2.4.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-GGmQVGpQWUe5qglJozEjZV/5dyxbOOZ0LHe/lqyWssB88Y4svNfst0uqBVscdDeIKl5Jy5+aPSvy7mI9tYRguw=="], + + "@types/statuses": ["@types/statuses@2.0.6", "", {}, "sha512-xMAgYwceFhRA2zY+XbEA7mxYbA093wdiW8Vu6gZPGWy9cmOyU9XesH1tNcEWsKFd5Vzrqx5T3D38PWx1FIIXkA=="], + "@types/trusted-types": ["@types/trusted-types@2.0.7", "", {}, "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw=="], "@types/unist": ["@types/unist@2.0.11", "", {}, "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA=="], @@ -731,6 +771,8 @@ "@vitejs/plugin-react": ["@vitejs/plugin-react@4.7.0", "", { "dependencies": { "@babel/core": "^7.28.0", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.27", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" } }, "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA=="], + "@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.6", "", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.6", "vitest": "3.2.6" }, "optionalPeers": ["@vitest/browser"] }, "sha512-LsAdmUapA0qSN306d8+zOyawM0hFm2m2Hg9IwVNIKBm+qJV8cijiq2c+gxKZcB1HCfIWAy+0qEZDCUQA58A1cw=="], + "@vitest/expect": ["@vitest/expect@3.2.6", "", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/spy": "3.2.6", "@vitest/utils": "3.2.6", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ=="], "@vitest/mocker": ["@vitest/mocker@3.2.6", "", { "dependencies": { "@vitest/spy": "3.2.6", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "optionalPeers": ["msw", "vite"] }, "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw=="], @@ -769,21 +811,25 @@ "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], + "ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="], + "asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="], "autoprefixer": ["autoprefixer@10.5.4", "", { "dependencies": { "browserslist": "^4.28.6", "caniuse-lite": "^1.0.30001806", "fraction.js": "^5.3.4", "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "peerDependencies": { "postcss": "^8.1.0" }, "bin": { "autoprefixer": "bin/autoprefixer" } }, "sha512-MaU0U/za7N3r6brxD4YB/l4NSrFzLPlANv6wEuQVaIPlD3L4W9rFcQPbL/EilY9BHhHvhfcz3gInDLrEtWT4EA=="], + "axe-core": ["axe-core@4.14.0", "", {}, "sha512-9WTZxEjsZ7b13TH8JPmbV2z8CHbl80/2hm3XPEG4JgNdQLK81IBRXmSxHfMAOkSqQeRxT/0dwNDz2GOm3zzpcQ=="], + "axios": ["axios@1.16.0", "", { "dependencies": { "follow-redirects": "^1.16.0", "form-data": "^4.0.5", "proxy-from-env": "^2.1.0" } }, "sha512-6hp5CwvTPlN2A31g5dxnwAX0orzM7pmCRDLnZSX772mv8WDqICwFjowHuPs04Mc8deIld1+ejhtaMn5vp6b+1w=="], "babel-plugin-macros": ["babel-plugin-macros@3.1.0", "", { "dependencies": { "@babel/runtime": "^7.12.5", "cosmiconfig": "^7.0.0", "resolve": "^1.19.0" } }, "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg=="], "bail": ["bail@2.0.2", "", {}, "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw=="], - "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], "baseline-browser-mapping": ["baseline-browser-mapping@2.9.19", "", { "bin": { "baseline-browser-mapping": "dist/cli.js" } }, "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg=="], - "brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + "brace-expansion": ["brace-expansion@5.0.12", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ=="], "browserslist": ["browserslist@4.28.7", "", { "dependencies": { "baseline-browser-mapping": "^2.10.44", "caniuse-lite": "^1.0.30001806", "electron-to-chromium": "^1.5.393", "node-releases": "^2.0.51", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw=="], @@ -821,10 +867,18 @@ "classnames": ["classnames@2.5.1", "", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="], + "cli-width": ["cli-width@4.1.0", "", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + "clone": ["clone@2.1.2", "", {}, "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w=="], "clsx": ["clsx@2.1.1", "", {}, "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + "color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="], "combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="], @@ -841,6 +895,8 @@ "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + "core-js": ["core-js@3.48.0", "", {}, "sha512-zpEHTy1fjTMZCKLHUZoVeylt9XrzaIN2rbPXEt0k+q7JE5CkCZdo6bNq55bn24a69CH7ErAVLKijxJja4fw+UQ=="], "cosmiconfig": ["cosmiconfig@8.3.6", "", { "dependencies": { "import-fresh": "^3.3.0", "js-yaml": "^4.1.0", "parse-json": "^5.2.0", "path-type": "^4.0.0" }, "peerDependencies": { "typescript": ">=4.9.5" }, "optionalPeers": ["typescript"] }, "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA=="], @@ -917,6 +973,8 @@ "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + "electron-to-chromium": ["electron-to-chromium@1.5.396", "", {}, "sha512-yHiw2Y3C3H9U6TMbOfoWK/BPreiOPXRfTWPBwQBoZG6/8TB6eOPnsy5oaRYuatR7Fw2SJ4kKforgufeo7fq0EQ=="], "emoji-picker-react": ["emoji-picker-react@4.17.2", "", { "dependencies": { "flairup": "1.0.0" }, "peerDependencies": { "react": ">=16" } }, "sha512-d70sXdqaorBsvYwNAjRUkV8VQUQTafN/XTjucl6lH2O/B+aFUiIZy5hpRJ0FxuzgVnkeJaydnRCrYpuGd0zhgQ=="], @@ -951,7 +1009,7 @@ "escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="], - "estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], "eventemitter3": ["eventemitter3@5.0.4", "", {}, "sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw=="], @@ -961,8 +1019,14 @@ "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-string-truncated-width": ["fast-string-truncated-width@3.0.3", "", {}, "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g=="], + + "fast-string-width": ["fast-string-width@3.0.2", "", { "dependencies": { "fast-string-truncated-width": "^3.0.2" } }, "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg=="], + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-wrap-ansi": ["fast-wrap-ansi@0.2.2", "", { "dependencies": { "fast-string-width": "^3.0.2" } }, "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q=="], + "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], "fflate": ["fflate@0.4.8", "", {}, "sha512-FJqqoDBR00Mdj9ppamLa/Y7vxm+PRmNWA67N846RvsoYVMKB4q3y/de5PA7gUmRMYK/8CMz2GDZQmCRN1wBcWA=="], @@ -975,6 +1039,8 @@ "follow-redirects": ["follow-redirects@1.16.0", "", {}, "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw=="], + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + "form-data": ["form-data@4.0.5", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w=="], "fraction.js": ["fraction.js@5.3.4", "", {}, "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ=="], @@ -993,22 +1059,28 @@ "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], "get-nonce": ["get-nonce@1.0.1", "", {}, "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q=="], "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], - "glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "graphql": ["graphql@16.14.2", "", {}, "sha512-Chq1s4CY7jmh8gO2qvLIJyfCDIN+EHLFW/9iShnp1z8FjBQMoodWP1kDC36VAMXXIvAjj4ARa7ntfAV2BrjsbA=="], + "handlebars": ["handlebars@4.7.9", "", { "dependencies": { "minimist": "^1.2.5", "neo-async": "^2.6.2", "source-map": "^0.6.1", "wordwrap": "^1.0.0" }, "optionalDependencies": { "uglify-js": "^3.1.4" }, "bin": { "handlebars": "bin/handlebars" } }, "sha512-4E71E0rpOaQuJR2A3xDZ+GM1HyWYv1clR58tC8emQNeQe3RH7MAzSbat+V0wG78LQBo6m6bzSG/L4pBuCsgnUQ=="], "happy-dom": ["happy-dom@20.10.2", "", { "dependencies": { "@types/node": ">=20.0.0", "@types/whatwg-mimetype": "^3.0.2", "@types/ws": "^8.18.1", "buffer-image-size": "^0.6.4", "entities": "^7.0.1", "whatwg-mimetype": "^3.0.0", "ws": "^8.21.0" } }, "sha512-5p9Sxis3eowDJKqx90QCsgbNA02XXqJ59NOHvD4V6cxp+rP4d/xOyVx7uY3hS8hiUbY1VeiFH8lbJ81AyuDVLQ=="], + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], "has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="], @@ -1023,8 +1095,12 @@ "hastscript": ["hastscript@9.0.1", "", { "dependencies": { "@types/hast": "^3.0.0", "comma-separated-tokens": "^2.0.0", "hast-util-parse-selector": "^4.0.0", "property-information": "^7.0.0", "space-separated-tokens": "^2.0.0" } }, "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w=="], + "headers-polyfill": ["headers-polyfill@5.0.1", "", { "dependencies": { "@types/set-cookie-parser": "^2.4.10", "set-cookie-parser": "^3.0.1" } }, "sha512-1TJ6Fih/b8h5TIcv+1+Hw0PDQWJTKDKzFZzcKOiW1wJza3XoAQlkCuXLbymPYB8+ZQyw8mHvdw560e8zVFIWyA=="], + "hoist-non-react-statics": ["hoist-non-react-statics@3.3.2", "", { "dependencies": { "react-is": "^16.7.0" } }, "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw=="], + "html-escaper": ["html-escaper@2.0.2", "", {}, "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg=="], + "https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="], "i18next": ["i18next@23.16.8", "", { "dependencies": { "@babel/runtime": "^7.23.2" } }, "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg=="], @@ -1063,10 +1139,22 @@ "is-hexadecimal": ["is-hexadecimal@2.0.1", "", {}, "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg=="], + "is-node-process": ["is-node-process@1.2.0", "", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="], + "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "istanbul-lib-coverage": ["istanbul-lib-coverage@3.2.2", "", {}, "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg=="], + + "istanbul-lib-report": ["istanbul-lib-report@3.0.1", "", { "dependencies": { "istanbul-lib-coverage": "^3.0.0", "make-dir": "^4.0.0", "supports-color": "^7.1.0" } }, "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw=="], + + "istanbul-lib-source-maps": ["istanbul-lib-source-maps@5.0.6", "", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.23", "debug": "^4.1.1", "istanbul-lib-coverage": "^3.0.0" } }, "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A=="], + + "istanbul-reports": ["istanbul-reports@3.2.0", "", { "dependencies": { "html-escaper": "^2.0.0", "istanbul-lib-report": "^3.0.0" } }, "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA=="], + + "jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], "js-cookie": ["js-cookie@3.0.8", "", {}, "sha512-yeJd4aNAdYZQjaon2bpD/Gb0B/omw7HQOsynXXcOiWVCacbBcPlgn8S/d1X6blFSaHao7ozqtW7NZW19xpCtIw=="], @@ -1143,7 +1231,9 @@ "magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="], - "make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + "magicast": ["magicast@0.3.5", "", { "dependencies": { "@babel/parser": "^7.25.4", "@babel/types": "^7.25.4", "source-map-js": "^1.2.0" } }, "sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ=="], + + "make-dir": ["make-dir@4.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw=="], "markdown-table": ["markdown-table@3.0.4", "", {}, "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw=="], @@ -1245,11 +1335,11 @@ "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], - "minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "minimatch": ["minimatch@10.2.6", "", { "dependencies": { "brace-expansion": "^5.0.8" } }, "sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A=="], "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], - "minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + "minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], "minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="], @@ -1269,6 +1359,10 @@ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msw": ["msw@2.15.0", "", { "dependencies": { "@inquirer/confirm": "^6.0.11", "@mswjs/interceptors": "^0.41.3", "@open-draft/deferred-promise": "^3.0.0", "@types/statuses": "^2.0.6", "cookie": "^1.1.1", "graphql": "^16.13.2", "headers-polyfill": "^5.0.1", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.11", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.1", "type-fest": "^5.5.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "hasInstallScript": true, "bin": { "msw": "cli/index.js" } }, "sha512-2wQAmKkQKxRuXvYJxVhPGG0wZNBQyD06oJvxqw90XqLvptdqxdlHrFUfEteKkpaNORX3Xzc+HtEl/q0nfmN2wQ=="], + + "mute-stream": ["mute-stream@3.0.0", "", {}, "sha512-dkEJPVvun4FryqBmZ5KhDo0K9iDXAwn08tMLDinNdRBNPcYEDiWYysLcc6k3mjTMlbP9KyylvRpd4wFtwrT9rw=="], + "nan": ["nan@2.25.0", "", {}, "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g=="], "nanoid": ["nanoid@3.3.16", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q=="], @@ -1291,6 +1385,10 @@ "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + "outvariant": ["outvariant@1.4.3", "", {}, "sha512-+Sl2UErvtsoajRDKCE5/dBz4DIvHXQQnAxtQTF04OJxY0+DyZXSo5P5Bb7XYWOh81syohlYL24hbDwxedPUJCA=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + "parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g=="], "parse-entities": ["parse-entities@4.0.2", "", { "dependencies": { "@types/unist": "^2.0.0", "character-entities-legacy": "^3.0.0", "character-reference-invalid": "^2.0.0", "decode-named-character-reference": "^1.0.0", "is-alphanumerical": "^2.0.0", "is-decimal": "^2.0.0", "is-hexadecimal": "^2.0.0" } }, "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw=="], @@ -1303,6 +1401,10 @@ "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "path-to-regexp": ["path-to-regexp@6.3.0", "", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="], + "path-type": ["path-type@4.0.0", "", {}, "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw=="], "path2d": ["path2d@0.1.1", "", {}, "sha512-/+S03c8AGsDYKKBtRDqieTJv2GlkMb0bWjnqOgtF6MkjdUQ9a8ARAtxWf9NgKLGm2+WQr6+/tqJdU8HNGsIDoA=="], @@ -1403,8 +1505,6 @@ "redux": ["redux@4.2.1", "", { "dependencies": { "@babel/runtime": "^7.9.2" } }, "sha512-LAUYz4lc+Do8/g7aeRa8JkyDErK6ekstQaqWQrNRW//MY1TvCEpMtpTWvlQ+FPbWCx+Xixu/6SHt5N0HR+SB4w=="], - "redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], - "refractor": ["refractor@5.0.0", "", { "dependencies": { "@types/hast": "^3.0.0", "@types/prismjs": "^1.0.0", "hastscript": "^9.0.0", "parse-entities": "^4.0.0" } }, "sha512-QXOrHQF5jOpjjLfiNk5GFnWhRXvxjUVnlFxkeDmewR5sXkr3iM46Zo+CnRR8B+MDVqkULW4EcLVcRBNOPXHosw=="], "remark-gfm": ["remark-gfm@3.0.1", "", { "dependencies": { "@types/mdast": "^3.0.0", "mdast-util-gfm": "^2.0.0", "micromark-extension-gfm": "^2.0.0", "unified": "^10.0.0" } }, "sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig=="], @@ -1413,6 +1513,8 @@ "remark-rehype": ["remark-rehype@10.1.0", "", { "dependencies": { "@types/hast": "^2.0.0", "@types/mdast": "^3.0.0", "mdast-util-to-hast": "^12.1.0", "unified": "^10.0.0" } }, "sha512-EFmR5zppdBp0WQeDVZ/b66CWJipB2q2VLNFMabzDSGR66Z2fQii83G5gTBbgGEnEEA0QRussvrFHxk1HWGJskw=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "reselect": ["reselect@5.1.1", "", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="], @@ -1421,6 +1523,8 @@ "resolve-from": ["resolve-from@4.0.0", "", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="], + "rettime": ["rettime@0.11.12", "", {}, "sha512-5ExtR1ppiDBQkVoPw5NAqdbfH4Iq2VOmQKHMtUoaFxA9bo2krzFYBtyq6K40xxbYp+2w/YOr6D+1ToF6tTN53A=="], + "rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="], "rollup": ["rollup@4.57.0", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.0", "@rollup/rollup-android-arm64": "4.57.0", "@rollup/rollup-darwin-arm64": "4.57.0", "@rollup/rollup-darwin-x64": "4.57.0", "@rollup/rollup-freebsd-arm64": "4.57.0", "@rollup/rollup-freebsd-x64": "4.57.0", "@rollup/rollup-linux-arm-gnueabihf": "4.57.0", "@rollup/rollup-linux-arm-musleabihf": "4.57.0", "@rollup/rollup-linux-arm64-gnu": "4.57.0", "@rollup/rollup-linux-arm64-musl": "4.57.0", "@rollup/rollup-linux-loong64-gnu": "4.57.0", "@rollup/rollup-linux-loong64-musl": "4.57.0", "@rollup/rollup-linux-ppc64-gnu": "4.57.0", "@rollup/rollup-linux-ppc64-musl": "4.57.0", "@rollup/rollup-linux-riscv64-gnu": "4.57.0", "@rollup/rollup-linux-riscv64-musl": "4.57.0", "@rollup/rollup-linux-s390x-gnu": "4.57.0", "@rollup/rollup-linux-x64-gnu": "4.57.0", "@rollup/rollup-linux-x64-musl": "4.57.0", "@rollup/rollup-openbsd-x64": "4.57.0", "@rollup/rollup-openharmony-arm64": "4.57.0", "@rollup/rollup-win32-arm64-msvc": "4.57.0", "@rollup/rollup-win32-ia32-msvc": "4.57.0", "@rollup/rollup-win32-x64-gnu": "4.57.0", "@rollup/rollup-win32-x64-msvc": "4.57.0", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-e5lPJi/aui4TO1LpAXIRLySmwXSE8k3b9zoGfd42p67wzxog4WHjiZF3M2uheQih4DGyc25QEV4yRBbpueNiUA=="], @@ -1435,6 +1539,8 @@ "set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="], + "set-cookie-parser": ["set-cookie-parser@3.2.2", "", {}, "sha512-df017qMeYuXo4emL09nlyMpi2i2MtEIKS/6jtXH3vmjFgIRsOi852/5RmhJ4EkvN3ZF8jXadM/EkTPXgc8yPIw=="], + "shallowequal": ["shallowequal@1.1.0", "", {}, "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ=="], "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], @@ -1443,7 +1549,7 @@ "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="], @@ -1471,14 +1577,22 @@ "state-local": ["state-local@1.0.7", "", {}, "sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w=="], + "statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="], + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + "strict-event-emitter": ["strict-event-emitter@0.5.1", "", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="], + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], "strip-literal": ["strip-literal@3.1.0", "", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="], @@ -1487,10 +1601,14 @@ "stylis": ["stylis@4.2.0", "", {}, "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw=="], + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], "svg-parser": ["svg-parser@2.0.4", "", {}, "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ=="], + "tagged-tag": ["tagged-tag@1.0.0", "", {}, "sha512-yEFYrVhod+hdNyx7g5Bnkkb0G6si8HJurOoOEgC8B/O0uXLHlaey/65KRv6cuWBNhBgHKAROVpc7QyYqE5gFng=="], + "tailwind-merge": ["tailwind-merge@3.6.0", "", {}, "sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w=="], "tailwindcss": ["tailwindcss@4.3.3", "", {}, "sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ=="], @@ -1501,6 +1619,8 @@ "tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="], + "test-exclude": ["test-exclude@7.0.2", "", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="], + "tiny-invariant": ["tiny-invariant@1.3.3", "", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="], "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], @@ -1515,6 +1635,12 @@ "tinyspy": ["tinyspy@4.0.4", "", {}, "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q=="], + "tldts": ["tldts@7.4.19", "", { "dependencies": { "tldts-core": "^7.4.19" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-48dH6aSpQnpbzDUc4vIQZ1VjB8tkK+e8c1yx05/SZK+rkssgjEOlxDd/yU8/a4vGYQ9vRgJr9dwMq9TJ0b3X2Q=="], + + "tldts-core": ["tldts-core@7.4.20", "", {}, "sha512-C630TWXZCZmLq0F2bW4QqwnaaxCQ/svpAqvJwnC4JvgEM5ohhBnGsEzTLSOXcN+xjz+kEaHS5uoWIw2klnqd0A=="], + + "tough-cookie": ["tough-cookie@6.0.2", "", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-exgYmnmL/sJpR3upZfXG5PoatXQii55xAiXGXzY+sROLZ/Y+SLcp9PgJNI9Vz37HpQ74WvDcLT8eqm+kV3FzrA=="], + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "trim-lines": ["trim-lines@3.0.1", "", {}, "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="], @@ -1523,6 +1649,8 @@ "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + "type-fest": ["type-fest@5.10.0", "", { "dependencies": { "tagged-tag": "^1.0.0" } }, "sha512-NoSdpq/WEiAg5sjmBkmV/hfxv6HJH4NqPNrqjtSO5CwRmpsDfaf4begxW34KdJykH/l1yHtwBWQkCRdoXO8mPA=="], + "typescript": ["typescript@7.0.2", "", { "optionalDependencies": { "@typescript/typescript-aix-ppc64": "7.0.2", "@typescript/typescript-darwin-arm64": "7.0.2", "@typescript/typescript-darwin-x64": "7.0.2", "@typescript/typescript-freebsd-arm64": "7.0.2", "@typescript/typescript-freebsd-x64": "7.0.2", "@typescript/typescript-linux-arm": "7.0.2", "@typescript/typescript-linux-arm64": "7.0.2", "@typescript/typescript-linux-loong64": "7.0.2", "@typescript/typescript-linux-mips64el": "7.0.2", "@typescript/typescript-linux-ppc64": "7.0.2", "@typescript/typescript-linux-riscv64": "7.0.2", "@typescript/typescript-linux-s390x": "7.0.2", "@typescript/typescript-linux-x64": "7.0.2", "@typescript/typescript-netbsd-arm64": "7.0.2", "@typescript/typescript-netbsd-x64": "7.0.2", "@typescript/typescript-openbsd-arm64": "7.0.2", "@typescript/typescript-openbsd-x64": "7.0.2", "@typescript/typescript-sunos-x64": "7.0.2", "@typescript/typescript-win32-arm64": "7.0.2", "@typescript/typescript-win32-x64": "7.0.2" }, "bin": { "tsc": "bin/tsc" } }, "sha512-8FYau96o3NKOhbjKi/qNvG/W5jhzxkbdm5sj9AbZ/5T5sWqn3hJgLfGx27sRKZWTvyzCP8dLRBTf5tBTSRVUNA=="], "uglify-js": ["uglify-js@3.19.3", "", { "bin": { "uglifyjs": "bin/uglifyjs" } }, "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ=="], @@ -1543,6 +1671,8 @@ "unist-util-visit-parents": ["unist-util-visit-parents@5.1.3", "", { "dependencies": { "@types/unist": "^2.0.0", "unist-util-is": "^5.0.0" } }, "sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg=="], + "until-async": ["until-async@3.0.2", "", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], "use-callback-ref": ["use-callback-ref@1.3.3", "", { "dependencies": { "tslib": "^2.0.0" }, "peerDependencies": { "@types/react": "*", "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" }, "optionalPeers": ["@types/react"] }, "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg=="], @@ -1597,16 +1727,26 @@ "wordwrap": ["wordwrap@1.0.0", "", {}, "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q=="], + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], "ws": ["ws@8.21.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g=="], "xmlhttprequest-ssl": ["xmlhttprequest-ssl@2.1.2", "", {}, "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ=="], + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], "yaml": ["yaml@1.10.2", "", {}, "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg=="], + "yargs": ["yargs@17.7.3", "", { "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" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + "zustand": ["zustand@4.5.7", "", { "dependencies": { "use-sync-external-store": "^1.2.2" }, "peerDependencies": { "@types/react": ">=16.8", "immer": ">=9.0.6", "react": ">=16.8" }, "optionalPeers": ["@types/react", "immer", "react"] }, "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw=="], "zwitch": ["zwitch@2.0.4", "", {}, "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A=="], @@ -1617,8 +1757,18 @@ "@emotion/babel-plugin/source-map": ["source-map@0.5.7", "", {}, "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ=="], + "@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="], + + "@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "@mapbox/node-pre-gyp/make-dir": ["make-dir@3.1.0", "", { "dependencies": { "semver": "^6.0.0" } }, "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw=="], + "@mapbox/node-pre-gyp/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "@mswjs/interceptors/@open-draft/deferred-promise": ["@open-draft/deferred-promise@2.2.0", "", {}, "sha512-CecwLWx3rhxVQF6V4bAgPS5t+So2sTbPgAzafKkVizyi7tlwpcFpdFqq+wqF2OwNBmqFuu6tOyouTuxgpMfzmA=="], + "@opentelemetry/otlp-transformer/@opentelemetry/resources": ["@opentelemetry/resources@2.2.0", "", { "dependencies": { "@opentelemetry/core": "2.2.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-1pNQf/JazQTMA0BiO5NINUzH0cbLbbl7mntLa4aJNmCCXSj0q03T5ZXXL0zw4G55TjdL9Tz32cznGClf+8zr5A=="], "@opentelemetry/resources/@opentelemetry/core": ["@opentelemetry/core@2.5.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-ka4H8OM6+DlUhSAZpONu0cPBtPPTQKxbxVzC4CzVx5+K4JnroJVBtDzLAMx4/3CDTJXRvVFhpFjtl4SaiTNoyQ=="], @@ -1633,6 +1783,10 @@ "@reduxjs/toolkit/redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], + "@reduxjs/toolkit/redux-thunk": ["redux-thunk@3.1.0", "", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="], + + "@rollup/pluginutils/estree-walker": ["estree-walker@2.0.2", "", {}, "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w=="], + "@svgr/hast-util-to-babel-ast/entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], "@tailwindcss/oxide-wasm32-wasi/@emnapi/core": ["@emnapi/core@1.11.3", "", { "dependencies": { "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" }, "bundled": true }, "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg=="], @@ -1651,7 +1805,7 @@ "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], - "@vitest/mocker/estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], + "ast-v8-to-istanbul/js-tokens": ["js-tokens@10.0.0", "", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="], "babel-plugin-macros/cosmiconfig": ["cosmiconfig@7.1.0", "", { "dependencies": { "@types/parse-json": "^4.0.0", "import-fresh": "^3.2.1", "parse-json": "^5.0.0", "path-type": "^4.0.0", "yaml": "^1.10.0" } }, "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA=="], @@ -1663,6 +1817,10 @@ "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], + "gauge/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "glob/minimatch": ["minimatch@9.0.9", "", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="], + "hast-util-parse-selector/@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], "hastscript/@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], @@ -1671,6 +1829,8 @@ "hoist-non-react-statics/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], + "make-dir/semver": ["semver@7.7.3", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q=="], + "mdast-util-find-and-replace/escape-string-regexp": ["escape-string-regexp@5.0.0", "", {}, "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw=="], "minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], @@ -1679,36 +1839,66 @@ "monaco-editor/dompurify": ["dompurify@3.2.7", "", { "optionalDependencies": { "@types/trusted-types": "^2.0.7" } }, "sha512-WhL/YuveyGXJaerVlMYGWhvQswa7myDG17P7Vu65EWC05o8vfeNbvNf4d/BOvH99+ZW+LlQsc1GDKMa1vNK6dw=="], + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + "pretty-format/react-is": ["react-is@17.0.2", "", {}, "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w=="], "prop-types/react-is": ["react-is@16.13.1", "", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="], "recharts/react-redux": ["react-redux@9.2.0", "", { "dependencies": { "@types/use-sync-external-store": "^0.0.6", "use-sync-external-store": "^1.4.0" }, "peerDependencies": { "@types/react": "^18.2.25 || ^19", "react": "^18.0 || ^19", "redux": "^5.0.0" }, "optionalPeers": ["@types/react", "redux"] }, "sha512-ROY9fvHhwOD9ySfrF0wmvu//bKCQ6AeZZq1nJNtbDC+kk5DuSuNX/n6YWYF/SYy7bSba4D4FSz8DJeKY/S/r+g=="], - "redux-thunk/redux": ["redux@5.0.1", "", {}, "sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w=="], - "refractor/@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], + "rimraf/glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="], + "string-width/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + "strip-literal/js-tokens": ["js-tokens@9.0.1", "", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="], + "tar/minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="], + "tar/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], "uvu/diff": ["diff@5.2.2", "", {}, "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A=="], "vite/postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="], + "wrap-ansi/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + "@babel/helper-compilation-targets/browserslist/caniuse-lite": ["caniuse-lite@1.0.30001766", "", {}, "sha512-4C0lfJ0/YPjJQHagaE9x2Elb69CIqEPZeG0anQt9SIvIoOH4a4uaRl73IavyO+0qZh6MDLH//DrXThEYKHkmYA=="], "@babel/helper-compilation-targets/browserslist/electron-to-chromium": ["electron-to-chromium@1.5.282", "", {}, "sha512-FCPkJtpst28UmFzd903iU7PdeVTfY0KAeJy+Lk0GLZRwgwYHn/irRcaCbQQOmr5Vytc/7rcavsYLvTM8RiHYhQ=="], "@babel/helper-compilation-targets/browserslist/node-releases": ["node-releases@2.0.27", "", {}, "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA=="], + "@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.5.0", "", {}, "sha512-zhvbbbdt1kbw7EJ9OQoxmQcuaNR9P5kTj7e/YdmOSU10aHWbycofJxA6jKfoEy3vlLdctOF9ZHaxLtmSm998wQ=="], + + "@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + + "@mapbox/node-pre-gyp/make-dir/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + "fs-minipass/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + "glob/minimatch/brace-expansion": ["brace-expansion@2.1.7", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g=="], + "recharts/react-redux/@types/use-sync-external-store": ["@types/use-sync-external-store@0.0.6", "", {}, "sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg=="], + "rimraf/glob/minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="], + "vite/postcss/nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="], + + "glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], } } diff --git a/frontend/package.json b/frontend/package.json index d59e29aaec..b8032f2e8e 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -16,6 +16,7 @@ "@rjsf/utils": "^5.8.1", "@rjsf/validator-ajv8": "^5.8.1", "@stripe/stripe-js": "^4.2.0", + "@tanstack/react-query": "5.104.5", "@tanstack/react-table": "^8.21.3", "axios": "1.16.0", "class-variance-authority": "^0.7.1", @@ -80,7 +81,7 @@ "typecheck": "tsc --noEmit", "typecheck:v1": "node scripts/check-v1.mjs --files && tsc -p src/v1/tsconfig.json", "lint:v1": "biome lint src/v1/ && node scripts/check-v1.mjs --code", - "test:v1": "vitest run --dir src/v1 --passWithNoTests && node --test test-fixtures/v1-boundary/boundary.fixtures.mjs" + "test:v1": "vitest run --config vitest.v1.config.mts && vitest run --config vitest.v1-plugins.config.mts && node --test test-fixtures/v1-boundary/boundary.fixtures.mjs test-fixtures/v1-harness/harness.fixtures.mjs" }, "browserslist": { "production": [ @@ -107,9 +108,12 @@ "@types/react": "^19", "@types/react-dom": "^19", "@vitejs/plugin-react": "^4.4.0", + "@vitest/coverage-v8": "3.2.6", "autoprefixer": "^10.5.4", + "axe-core": "4.14.0", "baseline-browser-mapping": "^2.9.19", "happy-dom": "^20.10.2", + "msw": "2.15.0", "postcss": "^8.5.23", "postcss-value-parser": "^4.2.0", "tailwindcss": "^4", diff --git a/frontend/src/v1/test/a11y.ts b/frontend/src/v1/test/a11y.ts new file mode 100644 index 0000000000..5ebe0844fc --- /dev/null +++ b/frontend/src/v1/test/a11y.ts @@ -0,0 +1,89 @@ +import axe from "axe-core"; +import { expect } from "vitest"; + +/* + * Accessibility assertions on axe-core. Two rules are off by default: + * `color-contrast` (tests load no CSS, so every colour is the browser + * default) and `region` (a component rendered alone is not inside a page's + * landmarks). Pass `rules` to turn either back on for a whole-page test. + */ + +export type A11yOptions = axe.RunOptions; + +const DEFAULT_RULES: NonNullable = { + "color-contrast": { enabled: false }, + region: { enabled: false }, +}; + +/** Runs axe over `target` and returns its violations. */ +export async function axeViolations( + target: Element = document.body, + options: A11yOptions = {}, +): Promise { + const results = await axe.run(target, { + ...options, + rules: { ...DEFAULT_RULES, ...options.rules }, + }); + return results.violations; +} + +/** One line per failing node: rule, impact, selector and axe's summary. */ +export function formatViolations(violations: readonly axe.Result[]): string { + return violations + .flatMap((violation) => + violation.nodes.map( + (node) => + ` [${violation.id}] (${violation.impact ?? "unknown"}) ${node.target.join(" ")}: ${violation.help}\n ${violation.helpUrl}`, + ), + ) + .join("\n"); +} + +/** Throws, listing every violation, when axe finds any in `target`. */ +export async function expectNoA11yViolations( + target: Element = document.body, + options: A11yOptions = {}, +): Promise { + const violations = await axeViolations(target, options); + if (violations.length > 0) { + throw new Error( + `Expected no accessibility violations, found:\n${formatViolations(violations)}`, + ); + } +} + +type A11yMatcherResult = { pass: boolean; message: () => string }; + +async function toHaveNoA11yViolations( + received: unknown, + options: A11yOptions = {}, +): Promise { + if (!(received instanceof Element)) { + return { + pass: false, + message: () => "toHaveNoA11yViolations expects a DOM element", + }; + } + const violations = await axeViolations(received, options); + return { + pass: violations.length === 0, + message: () => + violations.length === 0 + ? "Expected accessibility violations, found none" + : `Expected no accessibility violations, found:\n${formatViolations(violations)}`, + }; +} + +/** Registers `await expect(element).toHaveNoA11yViolations()`; setup.ts calls it. */ +export function registerA11yMatchers(): void { + expect.extend({ toHaveNoA11yViolations }); +} + +interface A11yMatchers { + toHaveNoA11yViolations(options?: A11yOptions): Promise; +} + +declare module "vitest" { + interface Assertion extends A11yMatchers {} + interface AsymmetricMatchersContaining extends A11yMatchers {} +} diff --git a/frontend/src/v1/test/dom.ts b/frontend/src/v1/test/dom.ts new file mode 100644 index 0000000000..b82dbc4e46 --- /dev/null +++ b/frontend/src/v1/test/dom.ts @@ -0,0 +1,209 @@ +import { act } from "react"; +import { vi } from "vitest"; + +/* + * Controllable stand-ins for browser APIs happy-dom does not drive. Each + * install uses vi.stubGlobal, which the v1 config undoes after every test + * (`unstubGlobals: true`), so no test needs its own afterEach. + */ + +type Size = Readonly<{ width: number; height: number }>; + +export type ResizeObserverControl = Readonly<{ + /** Elements currently observed, across every observer. */ + observed: () => Element[]; + /** Reports a new size for `target` to every observer watching it. */ + resize: (target: Element, size: Size) => void; +}>; + +/** Replaces ResizeObserver with one the test drives through `resize`. */ +export function installResizeObserver(): ResizeObserverControl { + const observers = new Set(); + + class FakeResizeObserver implements ResizeObserver { + readonly targets = new Set(); + constructor(private readonly callback: ResizeObserverCallback) { + observers.add(this); + } + observe(target: Element): void { + this.targets.add(target); + } + unobserve(target: Element): void { + this.targets.delete(target); + } + disconnect(): void { + this.targets.clear(); + observers.delete(this); + } + notify(target: Element, { width, height }: Size): void { + const contentRect = new DOMRect(0, 0, width, height); + const boxSize = [{ inlineSize: width, blockSize: height }]; + const entry = { + target, + contentRect, + borderBoxSize: boxSize, + contentBoxSize: boxSize, + devicePixelContentBoxSize: boxSize, + } as unknown as ResizeObserverEntry; + this.callback([entry], this); + } + } + + vi.stubGlobal("ResizeObserver", FakeResizeObserver); + return { + observed: () => [...observers].flatMap((o) => [...o.targets]), + resize: (target, size) => { + act(() => { + for (const observer of observers) { + if (observer.targets.has(target)) { + observer.notify(target, size); + } + } + }); + }, + }; +} + +export type IntersectionObserverControl = Readonly<{ + /** Elements currently observed, across every observer. */ + observed: () => Element[]; + /** Reports `target` entering (true) or leaving (false) the viewport. */ + intersect: (target: Element, isIntersecting: boolean) => void; +}>; + +/** Replaces IntersectionObserver with one the test drives through `intersect`. */ +export function installIntersectionObserver(): IntersectionObserverControl { + const observers = new Set(); + + class FakeIntersectionObserver implements IntersectionObserver { + readonly root: Element | Document | null; + readonly rootMargin: string; + readonly scrollMargin = "0px"; + readonly thresholds: readonly number[]; + readonly targets = new Set(); + constructor( + private readonly callback: IntersectionObserverCallback, + options: IntersectionObserverInit = {}, + ) { + this.root = options.root ?? null; + this.rootMargin = options.rootMargin ?? "0px"; + const threshold = options.threshold ?? 0; + this.thresholds = Array.isArray(threshold) ? threshold : [threshold]; + observers.add(this); + } + observe(target: Element): void { + this.targets.add(target); + } + unobserve(target: Element): void { + this.targets.delete(target); + } + disconnect(): void { + this.targets.clear(); + observers.delete(this); + } + takeRecords(): IntersectionObserverEntry[] { + return []; + } + notify(target: Element, isIntersecting: boolean): void { + const rect = target.getBoundingClientRect(); + const entry = { + target, + isIntersecting, + intersectionRatio: isIntersecting ? 1 : 0, + boundingClientRect: rect, + intersectionRect: isIntersecting ? rect : new DOMRect(), + rootBounds: null, + time: performance.now(), + } as IntersectionObserverEntry; + this.callback([entry], this); + } + } + + vi.stubGlobal("IntersectionObserver", FakeIntersectionObserver); + return { + observed: () => [...observers].flatMap((o) => [...o.targets]), + intersect: (target, isIntersecting) => { + act(() => { + for (const observer of observers) { + if (observer.targets.has(target)) { + observer.notify(target, isIntersecting); + } + } + }); + }, + }; +} + +export type MatchMediaControl = Readonly<{ + /** Flips `(prefers-color-scheme: dark)` and notifies its listeners. */ + setDark: (dark: boolean) => void; + /** Number of change listeners currently attached, across all queries. */ + listenerCount: () => number; +}>; + +type MediaListener = (event: MediaQueryListEvent) => void; + +const DARK_QUERY = "(prefers-color-scheme: dark)"; + +/** + * Replaces matchMedia. Only `(prefers-color-scheme: dark)` is ever true, and + * only while `dark` is set; every other query reports false. + */ +export function mockMatchMedia({ + dark = false, +}: { + dark?: boolean; +} = {}): MatchMediaControl { + let prefersDark = dark; + const listeners = new Map>(); + const listenersFor = (query: string) => { + let set = listeners.get(query); + if (!set) { + set = new Set(); + listeners.set(query, set); + } + return set; + }; + const matches = (query: string) => query === DARK_QUERY && prefersDark; + + vi.stubGlobal( + "matchMedia", + (query: string): MediaQueryList => + ({ + media: query, + get matches() { + return matches(query); + }, + onchange: null, + addEventListener: (_type: string, listener: MediaListener) => { + listenersFor(query).add(listener); + }, + removeEventListener: (_type: string, listener: MediaListener) => { + listenersFor(query).delete(listener); + }, + addListener: (listener: MediaListener) => { + listenersFor(query).add(listener); + }, + removeListener: (listener: MediaListener) => { + listenersFor(query).delete(listener); + }, + dispatchEvent: () => true, + }) as unknown as MediaQueryList, + ); + + return { + setDark: (value) => { + prefersDark = value; + act(() => { + for (const listener of listenersFor(DARK_QUERY)) { + listener({ + matches: value, + media: DARK_QUERY, + } as MediaQueryListEvent); + } + }); + }, + listenerCount: () => + [...listeners.values()].reduce((sum, set) => sum + set.size, 0), + }; +} diff --git a/frontend/src/v1/test/harness.test.tsx b/frontend/src/v1/test/harness.test.tsx new file mode 100644 index 0000000000..339b2b9c30 --- /dev/null +++ b/frontend/src/v1/test/harness.test.tsx @@ -0,0 +1,311 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { screen } from "@testing-library/react"; +import { + apiPath, + axeViolations, + DEFAULT_SESSION, + expectNoA11yViolations, + HttpResponse, + http, + installIntersectionObserver, + installResizeObserver, + makeSession, + mockApi, + mockMatchMedia, + PORTAL_CONTAINER_ID, + renderHookV1, + renderV1, +} from "@v1/test"; +import { useEffect, useState } from "react"; +import { createPortal } from "react-dom"; +import { useLocation, useParams } from "react-router-dom"; +import { describe, expect, it } from "vitest"; +import { takeUnhandledRequests, unhandledRequestsMessage } from "./network"; + +describe("renderV1", () => { + it("wraps the UI in the v1 root with the theme, and creates a themed portal", () => { + const { portal } = renderV1(

hello

, { theme: "dark" }); + const root = screen.getByText("hello").parentElement as HTMLElement; + expect(root).toHaveClass("v1"); + expect(root.dataset.uiVersion).toBe("v1"); + expect(root.dataset.theme).toBe("dark"); + expect(portal.id).toBe(PORTAL_CONTAINER_ID); + expect(portal).toHaveClass("v1"); + expect(portal.dataset.theme).toBe("dark"); + }); + + it("removes the portal between tests", () => { + expect(document.getElementById(PORTAL_CONTAINER_ID)).toBeNull(); + renderV1(

again

); + expect(document.getElementById(PORTAL_CONTAINER_ID)?.dataset.theme).toBe( + "light", + ); + }); + + it("renders portalled content into #v1-portal", () => { + function Menu() { + return createPortal( +
items
, + document.getElementById(PORTAL_CONTAINER_ID) as HTMLElement, + ); + } + const { portal } = renderV1(); + expect(portal).toContainElement(screen.getByRole("menu")); + }); + + it("starts the router at `route` and resolves params under `path`", () => { + function Page() { + const { id } = useParams(); + const location = useLocation(); + return ( +

+ {id} at {location.pathname} +

+ ); + } + renderV1(, { route: "/workflows/42", path: "/workflows/:id" }); + expect(screen.getByText("42 at /workflows/42")).toBeInTheDocument(); + }); + + it("returns the session and distribution it rendered with", () => { + const session = makeSession({ organization: { id: "org-9" } }); + const harness = renderV1(

x

, { session, distribution: "cloud" }); + expect(harness.session?.organization).toEqual({ + id: "org-9", + name: DEFAULT_SESSION.organization.name, + }); + expect(harness.distribution).toBe("cloud"); + expect(renderV1(

y

, { session: null }).session).toBeNull(); + }); + + it("gives each render a fresh query client with retries off", () => { + const first = renderV1(

a

).queryClient; + const second = renderV1(

b

).queryClient; + expect(first).not.toBe(second); + expect(first.getDefaultOptions().queries?.retry).toBe(false); + expect(first.getDefaultOptions().mutations?.retry).toBe(false); + }); + + it("returns a user-event instance bound to the render", async () => { + function Counter() { + const [count, setCount] = useState(0); + return ( + + ); + } + const { user } = renderV1(); + await user.click(screen.getByRole("button")); + expect(screen.getByRole("button")).toHaveTextContent("1"); + }); +}); + +describe("renderHookV1", () => { + it("runs the hook inside the providers", () => { + const { result, queryClient } = renderHookV1(() => useQueryClient()); + expect(result.current).toBe(queryClient); + }); + + it("passes initialProps", () => { + const { result, rerender } = renderHookV1((n: number) => n * 2, { + initialProps: 2, + }); + expect(result.current).toBe(4); + rerender(5); + expect(result.current).toBe(10); + }); +}); + +describe("network mocking", () => { + it("serves typed handlers registered with mockApi", async () => { + type Adapter = { id: string; name: string }; + const url = apiPath(DEFAULT_SESSION, "/adapter/"); + expect(url).toBe("/api/v1/unstract/org-1/adapter/"); + mockApi( + http.get(url, () => + HttpResponse.json([{ id: "a1", name: "OpenAI" }]), + ), + ); + function Adapters() { + const { data } = useQuery({ + queryKey: ["adapters"], + queryFn: async (): Promise => (await fetch(url)).json(), + }); + return ( +
    + {data?.map((a) => ( +
  • {a.name}
  • + ))} +
+ ); + } + renderV1(); + expect(await screen.findByText("OpenAI")).toBeInTheDocument(); + }); + + it("records an unmocked request with its method and absolute URL", async () => { + await expect(fetch("/api/v1/unmocked?x=1")).rejects.toThrow(); + const unhandled = takeUnhandledRequests(); + expect(unhandled).toEqual([ + "GET http://localhost:3000/api/v1/unmocked?x=1", + ]); + expect(unhandledRequestsMessage(unhandled)).toContain( + "Unmocked request (add a handler", + ); + }); + + it("errors an unmocked request made by a component, and names it", async () => { + function Loader() { + const [state, setState] = useState("loading"); + useEffect(() => { + fetch("/api/v1/unstract/org-1/missing/").then( + () => setState("loaded"), + () => setState("failed"), + ); + }, []); + return

{state}

; + } + renderV1(); + expect(await screen.findByText("failed")).toBeInTheDocument(); + // Drained here so this test passes; setup.ts would otherwise fail it. + expect(takeUnhandledRequests()).toEqual([ + "GET http://localhost:3000/api/v1/unstract/org-1/missing/", + ]); + }); + + it("answers an unmocked XHR (axios's transport) with a network error", async () => { + const outcome = await new Promise((resolve) => { + const xhr = new XMLHttpRequest(); + xhr.open("POST", "/api/v1/xhr"); + xhr.onload = () => resolve(`loaded ${xhr.status}`); + xhr.onerror = () => resolve("error"); + xhr.send("{}"); + }); + expect(outcome).toBe("error"); + expect(takeUnhandledRequests()).toEqual([ + "POST http://localhost:3000/api/v1/xhr", + ]); + }); + + it("pluralises the message for several requests", () => { + expect(unhandledRequestsMessage(["GET /a", "POST /b"])).toBe( + "Unmocked requests (add a handler with mockApi() or server.use()):\n GET /a\n POST /b", + ); + }); +}); + +describe("accessibility assertions", () => { + it("passes accessible markup", async () => { + const { container } = renderV1( + , + ); + await expect(container).toHaveNoA11yViolations(); + await expectNoA11yViolations(container); + }); + + it("names the rule and the node for a violation", async () => { + const { container } = renderV1(); + const violations = await axeViolations(container); + expect(violations.map((v) => v.id)).toContain("label"); + await expect(expectNoA11yViolations(container)).rejects.toThrow( + /\[label\] \(critical\) input/, + ); + await expect(expect(container).toHaveNoA11yViolations()).rejects.toThrow( + /Expected no accessibility violations/, + ); + }); + + it("rejects a non-element", async () => { + await expect(expect("text").toHaveNoA11yViolations()).rejects.toThrow( + /expects a DOM element/, + ); + }); + + it("supports .not for a known violation", async () => { + // A button with no accessible name: axe's `button-name`. + const { container } = renderV1(); + const results = await Promise.all([ + axeViolations(container), + axeViolations(container), + expectNoA11yViolations(container), + ]); + expect(results.slice(0, 2)).toEqual([[], []]); + }); + + it("keeps the queue going after a failed run", async () => { + vi.spyOn(axe, "run").mockRejectedValueOnce(new Error("axe broke")); + await expect(axeViolations(container())).rejects.toThrow("axe broke"); + expect(await axeViolations(container())).toEqual([]); + }); }); +function container(): Element { + return renderV1(

text

).container; +} + describe("DOM stand-ins", () => { it("drives ResizeObserver callbacks", () => { const control = installResizeObserver(); diff --git a/frontend/src/v1/test/network.ts b/frontend/src/v1/test/network.ts index 9e68988fb9..09b4d0d110 100644 --- a/frontend/src/v1/test/network.ts +++ b/frontend/src/v1/test/network.ts @@ -1,32 +1,51 @@ -import { HttpResponse, http } from "msw"; +import { + HttpResponse, + http, + type RequestHandler, + type WebSocketHandler, + ws, +} from "msw"; import { setupServer } from "msw/node"; import type { TestSession } from "./session"; /* - * Network mocking at the HTTP boundary. MSW intercepts fetch and XHR (so - * axios too). setup.ts starts the server, resets handlers after each test, - * and fails the test if any request went unmocked. + * Network mocking at the HTTP boundary. MSW intercepts fetch, XHR (so axios + * too) and WebSocket. setup.ts starts the server, resets handlers after each + * test, and fails the test if any request went unmocked. */ -export { delay, HttpResponse, http } from "msw"; +export { delay, HttpResponse, http, ws } from "msw"; const unhandledRequests: string[] = []; /* - * Unmocked requests are answered by a catch-all handler, not by MSW's + * Unmocked requests are answered by catch-all handlers, not by MSW's * `onUnhandledRequest: "error"`. Under happy-dom that strategy does not stop - * the request: happy-dom's fetch falls through to Node's http, which MSW - * intercepts a second time, and then the request really goes out. A handler - * that returns a network error ends it at the first interception. Handlers - * added with server.use() are prepended, so this one always matches last, - * and resetHandlers() keeps it. + * an HTTP request: happy-dom's fetch falls through to Node's http, which MSW + * intercepts a second time, and then the request really goes out. For a + * WebSocket it only logs, and the mocked socket opens. A handler that + * returns a network error, or closes the socket, ends it in-process. */ -const unmockedRequestHandler = http.all("*", ({ request }) => { - unhandledRequests.push(`${request.method} ${request.url}`); - return HttpResponse.error(); -}); +const unmockedHandlers: (RequestHandler | WebSocketHandler)[] = [ + http.all("*", ({ request }) => { + unhandledRequests.push(`${request.method} ${request.url}`); + return HttpResponse.error(); + }), + ws.link(/.*/).addEventListener("connection", ({ client }) => { + unhandledRequests.push(`WEBSOCKET ${client.url.href}`); + client.close(1011, "Unmocked WebSocket"); + }), +]; -export const server = setupServer(unmockedRequestHandler); +export const server = setupServer(...unmockedHandlers); + +/* + * server.use() prepends, so the catch-alls always match last. resetHandlers + * with arguments would replace them, so it is pinned to re-append them. + */ +const resetHandlers = server.resetHandlers.bind(server); +server.resetHandlers = (...handlers) => + resetHandlers(...handlers, ...unmockedHandlers); /** Returns and clears the requests no handler matched since the last call. */ export function takeUnhandledRequests(): string[] { diff --git a/frontend/src/v1/test/setup.ts b/frontend/src/v1/test/setup.ts index b4a0e71d42..f30d9e790a 100644 --- a/frontend/src/v1/test/setup.ts +++ b/frontend/src/v1/test/setup.ts @@ -1,6 +1,6 @@ import "@testing-library/jest-dom/vitest"; import { cleanup } from "@testing-library/react"; -import { afterAll, afterEach, beforeAll } from "vitest"; +import { afterAll, afterEach, beforeAll, vi } from "vitest"; import { registerA11yMatchers } from "./a11y"; import { server, @@ -18,23 +18,48 @@ import { removePortalContainer } from "./providers"; registerA11yMatchers(); beforeAll(() => { - // The catch-all handler in network.ts answers every unmocked HTTP request, - // so this only fires for what it cannot match; fail loudly there too. + // The catch-all handlers in network.ts answer every unmocked HTTP request + // and WebSocket, so this only fires for what they cannot match. server.listen({ onUnhandledRequest: "error" }); }); -afterEach(() => { +/** Lets requests already started (e.g. by an unmount effect) reach MSW. */ +async function settle(): Promise { + // A test that left fake timers on would stall the wait below forever. + if (vi.isFakeTimers()) { + vi.useRealTimers(); + } + for (let turn = 0; turn < 2; turn += 1) { + await new Promise((resolve) => setTimeout(resolve, 0)); + } +} + +function failOnUnhandledRequests(): void { + const unhandled = takeUnhandledRequests(); + if (unhandled.length > 0) { + throw new Error(unhandledRequestsMessage(unhandled)); + } +} + +afterEach(async () => { + // Unmount first: cleanup effects may make requests, which belong to this + // test and must still see its handlers. cleanup(); removePortalContainer(); + await settle(); server.resetHandlers(); // Thrown from afterEach so it fails the test even when the code under test // caught the network error. - const unhandled = takeUnhandledRequests(); - if (unhandled.length > 0) { - throw new Error(unhandledRequestsMessage(unhandled)); - } + failOnUnhandledRequests(); }); -afterAll(() => { - server.close(); +afterAll(async () => { + // Requests made after the last test's afterEach (a timer firing during the + // file's own afterAll) fail the file rather than vanish. + await settle(); + try { + failOnUnhandledRequests(); + } finally { + server.close(); + } }); diff --git a/frontend/test-fixtures/v1-harness/cases/fake-timers-left-on.test.ts b/frontend/test-fixtures/v1-harness/cases/fake-timers-left-on.test.ts new file mode 100644 index 0000000000..8b3bb32a10 --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/fake-timers-left-on.test.ts @@ -0,0 +1,11 @@ +import { expect, it, vi } from "vitest"; + +// Known good: a test that leaves fake timers on must not hang the harness. +it("leaves fake timers on", () => { + vi.useFakeTimers(); + expect(vi.isFakeTimers()).toBe(true); +}); + +it("runs on real timers afterwards", () => { + expect(vi.isFakeTimers()).toBe(false); +}); diff --git a/frontend/test-fixtures/v1-harness/cases/reset-handlers-with-args.test.ts b/frontend/test-fixtures/v1-harness/cases/reset-handlers-with-args.test.ts new file mode 100644 index 0000000000..5b76c36f7b --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/reset-handlers-with-args.test.ts @@ -0,0 +1,9 @@ +import { HttpResponse, http, server } from "@v1/test"; +import { expect, it } from "vitest"; + +// Known bad: resetHandlers with arguments must not disable the guard. +it("resets to its own handlers, then calls an unmocked URL", async () => { + server.resetHandlers(http.get("/api/v1/known", () => HttpResponse.json({}))); + await (await fetch("/api/v1/known")).json(); + await expect(fetch("/api/v1/unstract/org-1/unknown/")).rejects.toThrow(); +}); diff --git a/frontend/test-fixtures/v1-harness/cases/unmocked-after-last-test.test.ts b/frontend/test-fixtures/v1-harness/cases/unmocked-after-last-test.test.ts new file mode 100644 index 0000000000..9409dd6faf --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/unmocked-after-last-test.test.ts @@ -0,0 +1,12 @@ +import { afterAll, it } from "vitest"; + +// Known bad: the request fires from a timer after the last test's afterEach. +afterAll(async () => { + await new Promise((resolve) => setTimeout(resolve, 100)); +}); + +it("schedules a late poll without a handler", () => { + setTimeout(() => { + fetch("/api/v1/unstract/org-1/poll/").catch(() => undefined); + }, 30); +}); diff --git a/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount-attribution.test.tsx b/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount-attribution.test.tsx new file mode 100644 index 0000000000..ae33a8f569 --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount-attribution.test.tsx @@ -0,0 +1,21 @@ +import { renderV1 } from "@v1/test"; +import { useEffect } from "react"; +import { it } from "vitest"; + +// Known bad, first test only: its unmount request must fail it, not the next. +it("first: requests on unmount", () => { + function Editor() { + useEffect( + () => () => { + fetch("/api/v1/unstract/org-1/unmount/").catch(() => undefined); + }, + [], + ); + return

editing

; + } + renderV1(); +}); + +it("second: makes no request", async () => { + await new Promise((resolve) => setTimeout(resolve, 50)); +}); diff --git a/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount.test.tsx b/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount.test.tsx new file mode 100644 index 0000000000..6ebfc105bf --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/unmocked-on-unmount.test.tsx @@ -0,0 +1,19 @@ +import { renderV1 } from "@v1/test"; +import { useEffect } from "react"; +import { it } from "vitest"; + +// Known bad: the only test in the file; its request starts during cleanup(). +it("reports a draft on unmount without a handler", () => { + function Editor() { + useEffect( + () => () => { + fetch("/api/v1/unstract/org-1/draft/", { method: "POST" }).catch( + () => undefined, + ); + }, + [], + ); + return

editing

; + } + renderV1(); +}); diff --git a/frontend/test-fixtures/v1-harness/cases/unmocked-websocket.test.ts b/frontend/test-fixtures/v1-harness/cases/unmocked-websocket.test.ts new file mode 100644 index 0000000000..2cc4424506 --- /dev/null +++ b/frontend/test-fixtures/v1-harness/cases/unmocked-websocket.test.ts @@ -0,0 +1,11 @@ +import { expect, it } from "vitest"; + +// Known bad: an unmocked WebSocket (v0's socket.io uses this transport). +it("opens a socket without a handler", async () => { + const closed = await new Promise((resolve) => { + const socket = new WebSocket("ws://localhost:3000/api/v1/socket"); + socket.onclose = () => resolve(true); + setTimeout(() => resolve(false), 1000); + }); + expect(closed).toBe(true); +}); diff --git a/frontend/test-fixtures/v1-harness/harness.fixtures.mjs b/frontend/test-fixtures/v1-harness/harness.fixtures.mjs index 799d893660..255a2e6225 100644 --- a/frontend/test-fixtures/v1-harness/harness.fixtures.mjs +++ b/frontend/test-fixtures/v1-harness/harness.fixtures.mjs @@ -25,7 +25,13 @@ function runCase(file) { ], { cwd: FRONTEND, encoding: "utf8", env: { ...process.env, CI: "1" } }, ); - return { status: result.status, output: `${result.stdout}${result.stderr}` }; + // Colour codes would split the reporter's lines the assertions match on. + const output = `${result.stdout}${result.stderr}`.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes + /\u001b\[[0-9;]*m/g, + "", + ); + return { status: result.status, output }; } const CASES = [ @@ -39,10 +45,43 @@ const CASES = [ fails: true, names: "POST http://localhost:3000/api/v1/unstract/org-1/workflow/", }, + { + file: "unmocked-websocket.test.ts", + fails: true, + names: "WEBSOCKET ws://localhost:3000/api/v1/socket", + }, + { + // The request starts in cleanup(), after the test body, in the last test. + file: "unmocked-on-unmount.test.tsx", + fails: true, + names: "POST http://localhost:3000/api/v1/unstract/org-1/draft/", + }, + { + // ...and fails the test that unmounted, not the one after it. + file: "unmocked-on-unmount-attribution.test.tsx", + fails: true, + names: "GET http://localhost:3000/api/v1/unstract/org-1/unmount/", + summary: /Tests\s+1 failed \| 1 passed/, + failed: "first: requests on unmount", + }, + { + // Fired by a timer after the last test's afterEach. + file: "unmocked-after-last-test.test.ts", + fails: true, + names: "GET http://localhost:3000/api/v1/unstract/org-1/poll/", + }, + { + // resetHandlers(...handlers) must keep the catch-all. + file: "reset-handlers-with-args.test.ts", + fails: true, + names: "GET http://localhost:3000/api/v1/unstract/org-1/unknown/", + }, { file: "mocked-fetch.test.ts", fails: false }, + // A test that leaves fake timers on must not hang the next one's teardown. + { file: "fake-timers-left-on.test.ts", fails: false }, ]; -for (const { file, fails, names } of CASES) { +for (const { file, fails, names, summary, failed } of CASES) { test(`${file} ${fails ? "fails, naming the request" : "passes"}`, () => { const { status, output } = runCase(file); if (fails) { @@ -54,5 +93,11 @@ for (const { file, fails, names } of CASES) { } else { assert.equal(status, 0, output); } + if (summary) { + assert.match(output, summary); + } + if (failed) { + assert.match(output, new RegExp(`FAIL .*> ${failed}`)); + } }); } From c8d8d1036b8c64ae79028500a0ba958b4419f15d Mon Sep 17 00:00:00 2001 From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:37:09 +0530 Subject: [PATCH 4/7] UN-4329 [FIX] Let mocked WebSockets through, reconnect observer fakes, export ws - The WebSocket catch-all steps aside when a test's own ws.link() matches the URL: MSW runs every matching ws handler, so it closed mocked sockets too. - Observer fakes re-register on observe() after disconnect(). - @v1/test re-exports ws. - The harness fixtures run the lockfile's vitest via process.execPath instead of resolving npx from PATH. --- frontend/src/v1/test/dom.ts | 4 ++ frontend/src/v1/test/harness.test.tsx | 37 +++++++++++++++++++ frontend/src/v1/test/index.ts | 1 + frontend/src/v1/test/network.ts | 25 +++++++++++-- .../v1-harness/harness.fixtures.mjs | 7 ++-- 5 files changed, 67 insertions(+), 7 deletions(-) diff --git a/frontend/src/v1/test/dom.ts b/frontend/src/v1/test/dom.ts index b82dbc4e46..7c5c72f7bc 100644 --- a/frontend/src/v1/test/dom.ts +++ b/frontend/src/v1/test/dom.ts @@ -26,6 +26,8 @@ export function installResizeObserver(): ResizeObserverControl { observers.add(this); } observe(target: Element): void { + // Re-registers an instance reused after disconnect(), as browsers allow. + observers.add(this); this.targets.add(target); } unobserve(target: Element): void { @@ -92,6 +94,8 @@ export function installIntersectionObserver(): IntersectionObserverControl { observers.add(this); } observe(target: Element): void { + // Re-registers an instance reused after disconnect(), as browsers allow. + observers.add(this); this.targets.add(target); } unobserve(target: Element): void { diff --git a/frontend/src/v1/test/harness.test.tsx b/frontend/src/v1/test/harness.test.tsx index 6b89ec3ffb..696ae160fb 100644 --- a/frontend/src/v1/test/harness.test.tsx +++ b/frontend/src/v1/test/harness.test.tsx @@ -16,6 +16,7 @@ import { renderHookV1, renderV1, server, + ws, } from "@v1/test"; import axe from "axe-core"; import { useEffect, useState } from "react"; @@ -201,6 +202,25 @@ describe("network mocking", () => { ]); }); + it("leaves a mocked WebSocket open and unrecorded", async () => { + const socket = ws.link("ws://localhost:3000/api/v1/socket"); + mockApi( + socket.addEventListener("connection", ({ client }) => { + client.addEventListener("message", (event) => { + client.send(`echo ${String(event.data)}`); + }); + }), + ); + const reply = await new Promise((resolve, reject) => { + const client = new WebSocket("ws://localhost:3000/api/v1/socket"); + client.onopen = () => client.send("hi"); + client.onmessage = (event) => resolve(String(event.data)); + client.onclose = (event) => reject(new Error(`closed ${event.code}`)); + }); + expect(reply).toBe("echo hi"); + expect(takeUnhandledRequests()).toEqual([]); + }); + it("keeps the catch-all when handlers are reset with arguments", async () => { server.resetHandlers(http.get("/api/v1/known", () => HttpResponse.json(1))); expect(await (await fetch("/api/v1/known")).json()).toBe(1); @@ -318,6 +338,23 @@ describe("DOM stand-ins", () => { expect(control.observed()).toEqual([]); }); + it("keeps driving an observer reused after disconnect()", () => { + const size = installResizeObserver(); + const view = installIntersectionObserver(); + const target = document.createElement("div"); + const seen: string[] = []; + const resize = new ResizeObserver(() => seen.push("resize")); + const intersect = new IntersectionObserver(() => seen.push("intersect")); + for (const observer of [resize, intersect]) { + observer.observe(target); + observer.disconnect(); + observer.observe(target); + } + size.resize(target, { width: 1, height: 1 }); + view.intersect(target, true); + expect(seen).toEqual(["resize", "intersect"]); + }); + it("drives the dark-scheme media query", () => { const control = mockMatchMedia(); const query = window.matchMedia("(prefers-color-scheme: dark)"); diff --git a/frontend/src/v1/test/index.ts b/frontend/src/v1/test/index.ts index c95a2dba55..e7c1bbc2ca 100644 --- a/frontend/src/v1/test/index.ts +++ b/frontend/src/v1/test/index.ts @@ -24,6 +24,7 @@ export { http, mockApi, server, + ws, } from "./network"; export { createTestQueryClient, diff --git a/frontend/src/v1/test/network.ts b/frontend/src/v1/test/network.ts index 09b4d0d110..303f84c756 100644 --- a/frontend/src/v1/test/network.ts +++ b/frontend/src/v1/test/network.ts @@ -26,15 +26,32 @@ const unhandledRequests: string[] = []; * WebSocket it only logs, and the mocked socket opens. A handler that * returns a network error, or closes the socket, ends it in-process. */ +const unmockedSocketHandler: WebSocketHandler = ws + .link(/.*/) + .addEventListener("connection", ({ client }) => { + // MSW runs EVERY matching WebSocket handler (HTTP stops at the first), so + // step aside when a test's own ws.link() handles this URL. + const mocked = server + .listHandlers() + .some( + (handler) => + handler !== unmockedSocketHandler && + handler.kind === "websocket" && + handler.test(client.url), + ); + if (mocked) { + return; + } + unhandledRequests.push(`WEBSOCKET ${client.url.href}`); + client.close(1011, "Unmocked WebSocket"); + }); + const unmockedHandlers: (RequestHandler | WebSocketHandler)[] = [ http.all("*", ({ request }) => { unhandledRequests.push(`${request.method} ${request.url}`); return HttpResponse.error(); }), - ws.link(/.*/).addEventListener("connection", ({ client }) => { - unhandledRequests.push(`WEBSOCKET ${client.url.href}`); - client.close(1011, "Unmocked WebSocket"); - }), + unmockedSocketHandler, ]; export const server = setupServer(...unmockedHandlers); diff --git a/frontend/test-fixtures/v1-harness/harness.fixtures.mjs b/frontend/test-fixtures/v1-harness/harness.fixtures.mjs index 255a2e6225..38111e82ae 100644 --- a/frontend/test-fixtures/v1-harness/harness.fixtures.mjs +++ b/frontend/test-fixtures/v1-harness/harness.fixtures.mjs @@ -11,13 +11,14 @@ import { test } from "node:test"; const FRONTEND = path.resolve(import.meta.dirname, "..", ".."); const CONFIG = path.join("test-fixtures", "v1-harness", "vitest.config.mts"); +// The lockfile's vitest, run by this same node: nothing resolved from PATH. +const VITEST = path.join(FRONTEND, "node_modules", "vitest", "vitest.mjs"); function runCase(file) { const result = spawnSync( - "npx", + process.execPath, [ - "--no-install", - "vitest", + VITEST, "run", "--config", CONFIG, From a2ccc559e3a3822eb552356269dbac7248d1c868 Mon Sep 17 00:00:00 2001 From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Wed, 4 Nov 2026 10:42:28 +0530 Subject: [PATCH 5/7] UN-4329 [FEAT] Render v1 tests inside the real ThemeProvider now that #2399 landed The theme slot in src/v1/test/providers.tsx swaps its stand-in for app/theme/ThemeProvider, the one-file change the harness was built for. prepareProviders stores the requested theme for it to hydrate and creates #v1-portal with lib/portal's getPortalContainer; resetProviders clears the portal, the theme storage key and the store after each test. PORTAL_CONTAINER_ID, V1_ROOT_CLASS and ThemeMode now come from lib/. --- frontend/src/v1/test/README.md | 21 ++++++---- frontend/src/v1/test/index.ts | 3 +- frontend/src/v1/test/providers.tsx | 67 +++++++++++++++--------------- frontend/src/v1/test/render.tsx | 4 +- frontend/src/v1/test/session.ts | 8 ++-- frontend/src/v1/test/setup.ts | 4 +- 6 files changed, 55 insertions(+), 52 deletions(-) diff --git a/frontend/src/v1/test/README.md b/frontend/src/v1/test/README.md index 4c902bb63a..612b7585b0 100644 --- a/frontend/src/v1/test/README.md +++ b/frontend/src/v1/test/README.md @@ -63,20 +63,23 @@ await user.click(screen.getByRole("button", { name: "Add" })); ### Providers, and swapping in the real ones -`providers.tsx` is the only file that knows the provider chain. Each slot is a -stand-in until its task lands: +`providers.tsx` is the only file that knows the provider chain. Each slot is +the app's own provider once its task has landed, and a stand-in until then: -| Slot | Stand-in today | Replace with, when it lands | +| Slot | Today | Replace with, when it lands | | --- | --- | --- | -| query | `QueryClientProvider` over `createTestQueryClient()` | the data layer's provider (P0-T05, UN-4313) | -| router | `MemoryRouter` (+ `Routes` when `path` is set) | the app router in memory mode, with the route registry (P0-T03, UN-4271) | -| theme | `div.v1[data-ui-version="v1"][data-theme]` + `#v1-portal` | `app/theme/ThemeProvider` (P0-T04, #2399). Set the theme store's preference from `context.theme` first | +| query | stand-in: `QueryClientProvider` over `createTestQueryClient()` | the data layer's provider (P0-T05, UN-4313) | +| router | stand-in: `MemoryRouter` (+ `Routes` when `path` is set) | the app router in memory mode, with the route registry (P0-T03, UN-4271) | +| theme | **real**: `app/theme/ThemeProvider` (P0-T04, #2399) | — | To swap one, change its component in `PROVIDER_SLOTS`. It receives the resolved `ProviderContext` (query client, route, path, session, distribution, -theme). Tests and `renderV1`'s options stay the same. When a real session -store or plugin registry exists, seed it from `context.session` and -`context.distribution` in the same file. +theme). Global state a provider reads goes in `prepareProviders` (set before +render) and `resetProviders` (undone after each test). The theme slot shows +the pattern: the preference is stored, then `ThemeProvider` hydrates it, and +the store and its storage key are reset afterwards. Tests and `renderV1`'s +options stay the same. When a real session store or plugin registry exists, +seed it from `context.session` and `context.distribution` there too. ## Network diff --git a/frontend/src/v1/test/index.ts b/frontend/src/v1/test/index.ts index e7c1bbc2ca..59019a64b4 100644 --- a/frontend/src/v1/test/index.ts +++ b/frontend/src/v1/test/index.ts @@ -3,6 +3,7 @@ * (`*.test.ts(x)`, `*.spec.ts(x)`) and test/ itself may import it. */ +export { PORTAL_CONTAINER_ID, V1_ROOT_CLASS } from "@v1/lib/portal"; export { type A11yOptions, axeViolations, @@ -28,10 +29,8 @@ export { } from "./network"; export { createTestQueryClient, - PORTAL_CONTAINER_ID, type ProviderContext, type ProviderSlot, - V1_ROOT_CLASS, } from "./providers"; export { type RenderV1Options, diff --git a/frontend/src/v1/test/providers.tsx b/frontend/src/v1/test/providers.tsx index 69ac4c9191..08b1242e21 100644 --- a/frontend/src/v1/test/providers.tsx +++ b/frontend/src/v1/test/providers.tsx @@ -1,18 +1,24 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { ThemeProvider } from "@v1/app/theme/ThemeProvider"; +import { getPortalContainer, PORTAL_CONTAINER_ID } from "@v1/lib/portal"; +import { THEME_STORAGE_KEY } from "@v1/lib/theme"; +import { useThemeStore } from "@v1/stores/theme"; import type { JSX, ReactNode } from "react"; import { MemoryRouter, Route, Routes } from "react-router-dom"; import type { Distribution, TestSession, ThemeMode } from "./session"; /* * The v1 provider chain for tests: the ONLY file to edit when a real provider - * lands. Each slot is a stand-in until its task merges: + * lands. A slot is the app's own provider once its task has merged, and a + * stand-in until then: * - * query P0-T05 data layer (UN-4313) → the app's QueryClient provider - * router P0-T03 router + registry (UN-4271) → the app's router, in memory - * theme P0-T04 tokens + theme (#2399) → app/theme/ThemeProvider + * query stand-in P0-T05 data layer (UN-4313) → the app's QueryClient provider + * router stand-in P0-T03 router + registry (UN-4271) → the app's router, in memory + * theme real app/theme/ThemeProvider (P0-T04, #2399) * - * Swap a slot by changing its entry in PROVIDER_SLOTS; renderV1 and every - * test stay as they are. See README.md in this folder. + * Swap a slot by changing its entry in PROVIDER_SLOTS (and, if it holds + * global state, prepareProviders / resetProviders); renderV1 and every test + * stay as they are. See README.md in this folder. */ /** Everything a slot may need, resolved from renderV1's options. */ @@ -41,28 +47,27 @@ export function createTestQueryClient(): QueryClient { }); } -/** The single element under that v1 dialogs, menus and toasts render into (#2399). */ -export const PORTAL_CONTAINER_ID = "v1-portal"; - -/** The class that scopes v1 styles (#2399). */ -export const V1_ROOT_CLASS = "v1"; - -/** Creates (or re-themes) the #v1-portal container, as #2399's getPortalContainer does. */ -export function ensurePortalContainer(theme: ThemeMode): HTMLElement { - let container = document.getElementById(PORTAL_CONTAINER_ID); - if (!container) { - container = document.createElement("div"); - container.id = PORTAL_CONTAINER_ID; - container.className = V1_ROOT_CLASS; - document.body.appendChild(container); - } - container.dataset.theme = theme; - return container; +/** + * Global state the providers read, set before render: the theme preference + * ThemeProvider hydrates from, and the #v1-portal container (created by the + * app's own getPortalContainer, themed like the root). Returns the portal. + */ +export function prepareProviders(context: ProviderContext): HTMLElement { + useThemeStore.getState().setPreference(context.theme); + const portal = getPortalContainer(); + portal.dataset.theme = context.theme; + return portal; } -/** Removes the portal container; setup.ts calls it after each test. */ -export function removePortalContainer(): void { +/** Undoes prepareProviders; setup.ts calls it after each test. */ +export function resetProviders(): void { document.getElementById(PORTAL_CONTAINER_ID)?.remove(); + try { + window.localStorage.removeItem(THEME_STORAGE_KEY); + } catch { + // No storage, so nothing was persisted. + } + useThemeStore.setState(useThemeStore.getInitialState(), true); } const QuerySlot: ProviderSlot = ({ context, children }) => ( @@ -83,15 +88,9 @@ const RouterSlot: ProviderSlot = ({ context, children }) => ( ); -// Matches #2399's ThemeProvider output: the v1 root, versioned and themed. -const ThemeSlot: ProviderSlot = ({ context, children }) => ( -
- {children} -
+// The app's v1 root; it hydrates the preference prepareProviders stored. +const ThemeSlot: ProviderSlot = ({ children }) => ( + {children} ); /** Outermost first. */ diff --git a/frontend/src/v1/test/render.tsx b/frontend/src/v1/test/render.tsx index 4a34a9a87b..b9be09bd50 100644 --- a/frontend/src/v1/test/render.tsx +++ b/frontend/src/v1/test/render.tsx @@ -9,8 +9,8 @@ import userEvent, { type UserEvent } from "@testing-library/user-event"; import type { ReactNode } from "react"; import { createTestQueryClient, - ensurePortalContainer, type ProviderContext, + prepareProviders, V1TestProviders, } from "./providers"; import { @@ -68,7 +68,7 @@ function prepare(options: RenderV1Options) { session: context.session, distribution: context.distribution, theme: context.theme, - portal: ensurePortalContainer(context.theme), + portal: prepareProviders(context), }; const wrapper = ({ children }: { children: ReactNode }) => ( {children} diff --git a/frontend/src/v1/test/session.ts b/frontend/src/v1/test/session.ts index 4f31ff5e4f..1b5ecc0259 100644 --- a/frontend/src/v1/test/session.ts +++ b/frontend/src/v1/test/session.ts @@ -1,9 +1,11 @@ +import type { ThemeMode } from "@v1/lib/theme"; + +/** A colour mode the v1 root can carry (#2399's ThemeMode). */ +export type { ThemeMode }; + /** The build a test renders under; cloud and on-prem builds carry v1 plugins. */ export type Distribution = "oss" | "cloud" | "onprem"; -/** A colour mode the v1 root can carry; mirrors ThemeMode from #2399. */ -export type ThemeMode = "light" | "dark" | "unstract"; - export type TestUser = Readonly<{ id: string; email: string; diff --git a/frontend/src/v1/test/setup.ts b/frontend/src/v1/test/setup.ts index f30d9e790a..7e70c8682f 100644 --- a/frontend/src/v1/test/setup.ts +++ b/frontend/src/v1/test/setup.ts @@ -7,7 +7,7 @@ import { takeUnhandledRequests, unhandledRequestsMessage, } from "./network"; -import { removePortalContainer } from "./providers"; +import { resetProviders } from "./providers"; /* * Runs before every v1 test file (vitest.v1.config.mts `setupFiles`). @@ -45,7 +45,7 @@ afterEach(async () => { // Unmount first: cleanup effects may make requests, which belong to this // test and must still see its handlers. cleanup(); - removePortalContainer(); + resetProviders(); await settle(); server.resetHandlers(); // Thrown from afterEach so it fails the test even when the code under test From 6d6df2ce1a543b76e1f85851fcd87686c8c7ca28 Mon Sep 17 00:00:00 2001 From: Ghost Jake <89829542+Deepak-Kesavan@users.noreply.github.com> Date: Wed, 4 Nov 2026 11:20:10 +0530 Subject: [PATCH 6/7] UN-4329 [MISC] Clear the two Sonar issues on the harness - setup.ts: settle() awaits two macrotask turns without an await-in-loop. - test_groups.py: build the manifest outside pytest.raises so only load_groups() can raise. --- frontend/src/v1/test/setup.ts | 11 ++++++++--- tests/rig/tests/test_groups.py | 3 ++- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/frontend/src/v1/test/setup.ts b/frontend/src/v1/test/setup.ts index 7e70c8682f..81f3dbd720 100644 --- a/frontend/src/v1/test/setup.ts +++ b/frontend/src/v1/test/setup.ts @@ -29,9 +29,14 @@ async function settle(): Promise { if (vi.isFakeTimers()) { vi.useRealTimers(); } - for (let turn = 0; turn < 2; turn += 1) { - await new Promise((resolve) => setTimeout(resolve, 0)); - } + // Two macrotask turns, one after the other; the unmount cases in + // test-fixtures/v1-harness fail if this stops being enough. + await nextTurn(); + await nextTurn(); +} + +function nextTurn(): Promise { + return new Promise((resolve) => setTimeout(resolve, 0)); } function failOnUnhandledRequests(): void { diff --git a/tests/rig/tests/test_groups.py b/tests/rig/tests/test_groups.py index cf4f892a9c..22c7601522 100644 --- a/tests/rig/tests/test_groups.py +++ b/tests/rig/tests/test_groups.py @@ -506,8 +506,9 @@ def test_vitest_config_rejected_on_other_runners(tmp_path: Path) -> None: @pytest.mark.parametrize("value", ['""', "[a.mts]", "3"]) def test_vitest_config_must_be_a_path(tmp_path: Path, value: str) -> None: + manifest = _vitest_manifest(tmp_path, f"vitest_config: {value}") with pytest.raises(ValueError, match="must be a non-empty path"): - load_groups(_vitest_manifest(tmp_path, f"vitest_config: {value}")) + load_groups(manifest) def test_vitest_config_must_exist(tmp_path: Path) -> None: From 0209198505dc83fb61c6f993d2952959ad24bd12 Mon Sep 17 00:00:00 2001 From: Rahul Johny <116638720+johnyrahul@users.noreply.github.com> Date: Wed, 4 Nov 2026 09:41:45 +0000 Subject: [PATCH 7/7] UN-4612 [FEAT] Gate v1 unit tests on the frontend-v1-unit group and its coverage threshold MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The `v1 · Unit tests` check runs vitest.v1.config.mts, the rig's frontend-v1-unit config, so a run below v1's coverage threshold fails, and uploads the coverage report. test-fixtures/v1-unit-gate proves the threshold fails passing tests below it and that the CI job and tests/groups.yaml name the same config. The Guard fixtures check now also runs the harness fixtures. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01HzRGGS1GBTPe9YzARhNtPE --- .github/workflows/ci-frontend-v1.yaml | 30 ++++-- frontend/package.json | 2 +- frontend/src/v1/README.md | 2 +- .../at-threshold/src/v1/lib/grade.test.ts | 12 +++ .../cases/at-threshold/src/v1/lib/grade.ts | 16 ++++ .../below-threshold/src/v1/lib/grade.test.ts | 6 ++ .../cases/below-threshold/src/v1/lib/grade.ts | 16 ++++ .../v1-unit-gate/unit-gate.fixtures.mjs | 91 +++++++++++++++++++ .../v1-unit-gate/vitest.config.mts | 28 ++++++ 9 files changed, 195 insertions(+), 8 deletions(-) create mode 100644 frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.test.ts create mode 100644 frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.ts create mode 100644 frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.test.ts create mode 100644 frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.ts create mode 100644 frontend/test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs create mode 100644 frontend/test-fixtures/v1-unit-gate/vitest.config.mts diff --git a/.github/workflows/ci-frontend-v1.yaml b/.github/workflows/ci-frontend-v1.yaml index e97d50c1b2..d8d8242a36 100644 --- a/.github/workflows/ci-frontend-v1.yaml +++ b/.github/workflows/ci-frontend-v1.yaml @@ -136,8 +136,12 @@ jobs: working-directory: frontend run: node scripts/check-v1-styles.mjs - # The v1 unit tests. P0-T08 (UN-4329) replaces this with the frontend-v1 - # unit group. + # The v1 unit tests: the rig's `frontend-v1-unit` group (tests/groups.yaml), + # run with the same config so CI and the rig cannot disagree. The config + # fails the run below v1's coverage threshold. Run here rather than through + # ci-test.yaml because its tiers skip frontend-only PRs. Cloud v1 plugins + # (vitest.v1-plugins.config.mts) are gated by the cloud mirror, not here: + # OSS has none. unit: name: v1 · Unit tests needs: changes @@ -159,7 +163,18 @@ jobs: - name: Run v1 unit tests working-directory: frontend # The lockfile's vitest, never a fetched one. - run: ./node_modules/.bin/vitest run --dir src/v1 --passWithNoTests + run: ./node_modules/.bin/vitest run --config vitest.v1.config.mts + + # The summary is in the log; the lcov report is for reading what a + # failed threshold missed. + - name: Upload v1 coverage + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: frontend-v1-coverage + path: frontend/coverage/v1/ + if-no-files-found: ignore + retention-days: 7 # One production build, then the bundle guard (no v1 chunk loads v0 or # antd) and the separation guard (no enterprise plugin code in this repo). @@ -194,8 +209,11 @@ jobs: working-directory: frontend run: node scripts/check-v1-separation.mjs - # Proves every guard above fails and passes when it should (node --test, - # Node 22 APIs). Spawns the real tools, two small Vite builds included. + # Proves every guard above fails and passes when it should, and that the + # unit harness (src/v1/test) fails a test that sends an unmocked request, + # and that the Unit tests check fails below the coverage threshold and runs + # the rig group's config (node --test, Node 22 APIs). Spawns the real tools, two small + # Vite builds included. guard-fixtures: name: v1 · Guard fixtures needs: changes @@ -221,4 +239,4 @@ jobs: - name: Run the guard fixtures working-directory: frontend - run: node --test test-fixtures/v1-boundary/boundary.fixtures.mjs + run: node --test test-fixtures/v1-boundary/boundary.fixtures.mjs test-fixtures/v1-harness/harness.fixtures.mjs test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs diff --git a/frontend/package.json b/frontend/package.json index 4b585119de..48ce36dda3 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -82,7 +82,7 @@ "tokens:v1": "bun scripts/build-v1-tokens.mjs", "typecheck:v1": "node scripts/check-v1.mjs --files && tsc -p src/v1/tsconfig.json", "lint:v1": "biome lint --error-on-warnings src/v1/ && node scripts/check-v1.mjs --code && node scripts/check-v1-styles.mjs", - "test:v1": "vitest run --config vitest.v1.config.mts && vitest run --config vitest.v1-plugins.config.mts && node --test test-fixtures/v1-boundary/boundary.fixtures.mjs test-fixtures/v1-harness/harness.fixtures.mjs", + "test:v1": "vitest run --config vitest.v1.config.mts && vitest run --config vitest.v1-plugins.config.mts && node --test test-fixtures/v1-boundary/boundary.fixtures.mjs test-fixtures/v1-harness/harness.fixtures.mjs test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs", "seam:v1": "node scripts/check-v1-seam.mjs --base origin/main", "separation:v1": "node scripts/check-v1-separation.mjs", "bundle:v1": "node scripts/check-v1-bundle.mjs" diff --git a/frontend/src/v1/README.md b/frontend/src/v1/README.md index 7798f126a0..0172d69ade 100644 --- a/frontend/src/v1/README.md +++ b/frontend/src/v1/README.md @@ -93,7 +93,7 @@ Every folder may import from itself. Test files (`*.test.ts(x)`, | `bun run typecheck:v1` | TypeScript-only files, styles only in `styles/`, allowed file types only, no symlinks, folder taxonomy; then `tsc` with `src/v1/tsconfig.json` | | `bun run lint:v1` | Biome on `src/v1/` (warnings fail), then imports in code and in `styles/` CSS (nothing outside v1, folder contract, and v0 reaching v1 only through `@v1`), PropTypes and `ant-` checks, then the styling policy (S1 to S8 above) | | `bun run tokens:v1` | Regenerates `styles/tokens.css` from `styles/tokens.json` (via `styles/tokens.ts`; runs under bun, which imports the TypeScript source directly) | -| `bun run test:v1` | v1 unit tests (`vitest.v1.config.mts`, then cloud plugins with `vitest.v1-plugins.config.mts`) with v1's own coverage threshold, then the guardrail fixtures in `test-fixtures/v1-boundary/` and `test-fixtures/v1-harness/`. The harness is documented in `test/README.md` | +| `bun run test:v1` | v1 unit tests (`vitest.v1.config.mts`, then cloud plugins with `vitest.v1-plugins.config.mts`) with v1's own coverage threshold, then the guardrail fixtures in `test-fixtures/v1-boundary/`, `test-fixtures/v1-harness/` and `test-fixtures/v1-unit-gate/` (the coverage threshold fails a run, and CI runs the rig group's config). The harness is documented in `test/README.md` | | `bun run seam:v1` | Seam guard: v1 work changes v0 (`src/` outside v1, `public/`) only at the files in `scripts/v1-seam-allowlist.json`, compared with `origin/main` | | `bun run separation:v1` | No enterprise plugin code: no folder under `plugins/` is tracked in the public repository | | `bun run bundle:v1` | Production build, then no v1 chunk (or a chunk it loads) holds a v0 module or antd | diff --git a/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.test.ts b/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.test.ts new file mode 100644 index 0000000000..59ae1d7b2b --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.test.ts @@ -0,0 +1,12 @@ +import { expect, it } from "vitest"; +import { grade } from "./grade"; + +it.each([ + [95, "A"], + [85, "B"], + [75, "C"], + [65, "D"], + [10, "F"], +])("grades %i as %s", (score, letter) => { + expect(grade(score)).toBe(letter); +}); diff --git a/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.ts b/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.ts new file mode 100644 index 0000000000..15ec1244d8 --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/cases/at-threshold/src/v1/lib/grade.ts @@ -0,0 +1,16 @@ +// Every branch is run by its test. +export function grade(score: number): string { + if (score >= 90) { + return "A"; + } + if (score >= 80) { + return "B"; + } + if (score >= 70) { + return "C"; + } + if (score >= 60) { + return "D"; + } + return "F"; +} diff --git a/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.test.ts b/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.test.ts new file mode 100644 index 0000000000..fb0fed54f9 --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.test.ts @@ -0,0 +1,6 @@ +import { expect, it } from "vitest"; +import { grade } from "./grade"; + +it("grades a top score", () => { + expect(grade(95)).toBe("A"); +}); diff --git a/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.ts b/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.ts new file mode 100644 index 0000000000..b8cebaa57b --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/cases/below-threshold/src/v1/lib/grade.ts @@ -0,0 +1,16 @@ +// Half of this file is never run by its test, so line coverage is ~50%. +export function grade(score: number): string { + if (score >= 90) { + return "A"; + } + if (score >= 80) { + return "B"; + } + if (score >= 70) { + return "C"; + } + if (score >= 60) { + return "D"; + } + return "F"; +} diff --git a/frontend/test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs b/frontend/test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs new file mode 100644 index 0000000000..2d5778a653 --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/unit-gate.fixtures.mjs @@ -0,0 +1,91 @@ +/* + * Proves the `v1 · Unit tests` check (ci-frontend-v1.yaml) gates what it + * should: it runs the rig's `frontend-v1-unit` group config, and that config + * fails a run below v1's coverage threshold even when every test passes. + * Run by `bun run test:v1` and the `v1 · Guard fixtures` check. + */ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { test } from "node:test"; + +const FRONTEND = path.resolve(import.meta.dirname, "..", ".."); +const REPO = path.resolve(FRONTEND, ".."); +const CONFIG = path.join("test-fixtures", "v1-unit-gate", "vitest.config.mts"); +// The lockfile's vitest, run by this same node: nothing resolved from PATH. +const VITEST = path.join(FRONTEND, "node_modules", "vitest", "vitest.mjs"); + +function runCase(name) { + const reports = mkdtempSync(path.join(tmpdir(), "v1-unit-gate-")); + try { + const result = spawnSync( + process.execPath, + [VITEST, "run", "--config", CONFIG], + { + cwd: FRONTEND, + encoding: "utf8", + env: { + ...process.env, + CI: "1", + V1_GATE_CASE: name, + V1_GATE_REPORTS: reports, + }, + }, + ); + const output = `${result.stdout}${result.stderr}`.replace( + // biome-ignore lint/suspicious/noControlCharactersInRegex: ANSI escapes + /\u001b\[[0-9;]*m/g, + "", + ); + return { status: result.status, output }; + } finally { + rmSync(reports, { recursive: true, force: true }); + } +} + +test("passing tests below the coverage threshold fail the gate", () => { + const { status, output } = runCase("below-threshold"); + assert.notEqual(status, 0, output); + assert.match(output, /Tests\s+1 passed/); + assert.match( + output, + /Coverage for lines \([\d.]+%\) does not meet global threshold \(80%\)/, + ); +}); + +test("passing tests at the coverage threshold pass the gate", () => { + const { status, output } = runCase("at-threshold"); + assert.equal(status, 0, output); +}); + +// The `frontend-v1-unit` block of tests/groups.yaml, up to the next group. +function groupBlock(yaml, name) { + const start = yaml.indexOf(`\n ${name}:\n`); + assert.notEqual(start, -1, `tests/groups.yaml has no ${name} group`); + const rest = yaml.slice(start + name.length + 5); + const end = rest.search(/\n {2}[a-z0-9-]+:\n/); + return end === -1 ? rest : rest.slice(0, end); +} + +test("the CI unit check runs the rig group's vitest config", () => { + const groups = readFileSync(path.join(REPO, "tests", "groups.yaml"), "utf8"); + const config = groupBlock(groups, "frontend-v1-unit").match( + /^\s+vitest_config:\s*(\S+)\s*$/m, + )?.[1]; + assert.ok(config, "frontend-v1-unit sets no vitest_config"); + + const workflow = readFileSync( + path.join(REPO, ".github", "workflows", "ci-frontend-v1.yaml"), + "utf8", + ); + const job = workflow.slice(workflow.indexOf("\n unit:\n")); + assert.ok(job.length > 1, "ci-frontend-v1.yaml has no `unit` job"); + const run = job.match(/^\s+run: (.*vitest run.*)$/m)?.[1]; + assert.ok(run, "the `unit` job runs no vitest"); + assert.ok( + run.includes(`--config ${config}`), + `the \`unit\` job runs \`${run}\`, not the group's config ${config}`, + ); +}); diff --git a/frontend/test-fixtures/v1-unit-gate/vitest.config.mts b/frontend/test-fixtures/v1-unit-gate/vitest.config.mts new file mode 100644 index 0000000000..db1ebf87b3 --- /dev/null +++ b/frontend/test-fixtures/v1-unit-gate/vitest.config.mts @@ -0,0 +1,28 @@ +import path from "node:path"; +import { defineV1Config } from "../../vitest.v1.config.mts"; + +/* + * The real v1 config, coverage threshold included, rooted at one case in + * ./cases (named by V1_GATE_CASE), whose own src/v1 tree stands in for the + * real one. Only the setup file is re-pointed, at the real harness, and the + * report and Vite's cache go to V1_GATE_REPORTS so a run never writes into + * the tree. Used only by unit-gate.fixtures.mjs. + */ +const FRONTEND = path.resolve(import.meta.dirname, "..", ".."); +const CASE = process.env.V1_GATE_CASE ?? ""; +const REPORTS = process.env.V1_GATE_REPORTS ?? ""; +if (!CASE || !REPORTS) { + throw new Error("Set V1_GATE_CASE and V1_GATE_REPORTS"); +} +const base = defineV1Config("core"); + +export default { + ...base, + root: path.join(import.meta.dirname, "cases", CASE), + cacheDir: path.join(REPORTS, "vite"), + test: { + ...base.test, + setupFiles: [path.join(FRONTEND, "src", "v1", "test", "setup.ts")], + coverage: { ...base.test?.coverage, reportsDirectory: REPORTS }, + }, +};