diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20304278..4572566f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,6 +46,9 @@ jobs: - name: Build frontend run: pnpm --filter frontend run build + - name: Run frontend tests + run: pnpm --filter frontend run test + - name: Run backend tests run: pnpm --filter github-readme-stats run test diff --git a/apps/frontend/package.json b/apps/frontend/package.json index d1dd0cbd..829fc685 100644 --- a/apps/frontend/package.json +++ b/apps/frontend/package.json @@ -31,14 +31,16 @@ "clsx": "2.1.1", "postcss": "^8.4.31", "tailwindcss": "^3.3.5", - "vite": "7.3.1", + "vite": "catalog:default", "vite-plugin-node-polyfills": "0.25.0", - "vite-plugin-string-replace": "^1.1.5" + "vite-plugin-string-replace": "1.1.5", + "vitest": "catalog:default" }, "scripts": { "dev": "vite", "build": "vite build", "preview": "vite preview", + "test": "vitest", "test:e2e": "playwright test", "typecheck": "tsc --noEmit" }, diff --git a/apps/frontend/src/pages/Home/Home.tsx b/apps/frontend/src/pages/Home/Home.tsx index a1f7b5e3..a471c34e 100644 --- a/apps/frontend/src/pages/Home/Home.tsx +++ b/apps/frontend/src/pages/Home/Home.tsx @@ -26,6 +26,7 @@ import { } from "../../redux/selectors/userSelectors"; import { login } from "../../redux/slices/user"; +import { getFullSuffix } from "./getFullSuffix"; import { CustomizeStage } from "./stages/Customize"; import { DisplayStage } from "./stages/Display"; import { LoginStage } from "./stages/Login/Login"; @@ -105,107 +106,27 @@ export function HomeScreen({ stage, setStage }: HomeScreenProps): JSX.Element { setStage(2); }; - let fullSuffix = `${selectedCard === CardType.STATS ? "" : "/" + selectedCard}?`; - - switch (selectedCard) { - case CardType.STATS: - case CardType.TOP_LANGS: - fullSuffix += `username=${selectedUserId}`; - break; - case CardType.PIN: - fullSuffix += `username=${userId}&repo=${repo}`; - break; - case CardType.GIST: - fullSuffix += `id=${gist}`; - break; - case CardType.WAKATIME: - fullSuffix += `username=${wakatimeUser}`; - break; - - default: - selectedCard satisfies never; - } - - if ( - selectedStatsRank !== STATS_DEFAULT_RANK && - selectedCard === CardType.STATS - ) { - fullSuffix += `&rank_icon=${selectedStatsRank.value}`; - } - - if ( - selectedLanguagesLayout !== LANGUAGES_DEFAULT_LAYOUT && - selectedCard === CardType.TOP_LANGS - ) { - fullSuffix += `&layout=${selectedLanguagesLayout.value}`; - } - - if ( - selectedWakatimeLayout !== WAKATIME_DEFAULT_LAYOUT && - selectedCard === CardType.WAKATIME - ) { - fullSuffix += `&layout=${selectedWakatimeLayout.value}`; - } - - if ( - !showTitle && - (selectedCard === CardType.STATS || - selectedCard === CardType.TOP_LANGS || - selectedCard === CardType.WAKATIME) - ) { - fullSuffix += "&hide_title=true"; - } - - if ( - showOwner && - (selectedCard === CardType.PIN || selectedCard === CardType.GIST) - ) { - fullSuffix += "&show_owner=true"; - } - - if (descriptionLines && selectedCard === CardType.PIN) { - fullSuffix += `&description_lines_count=${descriptionLines}`; - } - - if ( - customTitle && - (selectedCard === CardType.STATS || selectedCard === CardType.WAKATIME) - ) { - const encodedTitle = encodeURIComponent(customTitle); - fullSuffix += `&custom_title=${encodedTitle}`; - } - - if ( - langsCount && - (selectedCard === CardType.TOP_LANGS || selectedCard === CardType.WAKATIME) - ) { - fullSuffix += `&langs_count=${langsCount}`; - } - - if (showAllStats && selectedCard === CardType.STATS) { - fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`; - } - - if (showIcons && selectedCard === CardType.STATS) { - fullSuffix += `&show_icons=true`; - } - - if (includeAllCommits && selectedCard === CardType.STATS) { - fullSuffix += `&include_all_commits=true`; - } - - if ( - !enableAnimations && - (selectedCard === CardType.STATS || - selectedCard === CardType.TOP_LANGS || - selectedCard === CardType.WAKATIME) - ) { - fullSuffix += `&disable_animations=${!enableAnimations}`; - } - - if (usePercent && selectedCard === CardType.WAKATIME) { - fullSuffix += `&display_format=percent`; - } + const fullSuffix = getFullSuffix({ + userId, + selectedCard, + selectedUserId, + repo, + gist, + wakatimeUser, + selectedStatsRank, + selectedLanguagesLayout, + selectedWakatimeLayout, + showTitle, + showOwner, + descriptionLines, + customTitle, + langsCount, + showAllStats, + showIcons, + includeAllCommits, + enableAnimations, + usePercent, + }); // for stage four let themeSuffix = fullSuffix; diff --git a/apps/frontend/src/pages/Home/getFullSuffix.test.ts b/apps/frontend/src/pages/Home/getFullSuffix.test.ts new file mode 100644 index 00000000..aa22224f --- /dev/null +++ b/apps/frontend/src/pages/Home/getFullSuffix.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from "vitest"; + +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; +import { CardType } from "../../models/CardType"; + +import { getFullSuffix } from "./getFullSuffix"; + +const baseOptions = { + selectedCard: CardType.STATS, + selectedUserId: "john", + userId: "john-github", + repo: "repo1", + gist: "gist1", + wakatimeUser: "wakaUser", + selectedStatsRank: STATS_DEFAULT_RANK, + selectedLanguagesLayout: LANGUAGES_DEFAULT_LAYOUT, + selectedWakatimeLayout: WAKATIME_DEFAULT_LAYOUT, + showTitle: true, + showOwner: false, + descriptionLines: undefined, + customTitle: "", + langsCount: undefined, + showAllStats: false, + showIcons: false, + includeAllCommits: false, + enableAnimations: true, + usePercent: false, +}; + +describe("getFullSuffix", () => { + it("builds stats suffix with defaults", () => { + const result = getFullSuffix(baseOptions); + + expect(result).toBe("?username=john"); + }); + + it("adds stats options", () => { + const result = getFullSuffix({ + ...baseOptions, + showIcons: true, + includeAllCommits: true, + showAllStats: true, + showTitle: false, + }); + + expect(result).toBe( + "?username=john" + + "&hide_title=true" + + "&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented" + + "&show_icons=true" + + "&include_all_commits=true", + ); + }); + + it("builds top-langs suffix", () => { + const result = getFullSuffix({ + ...baseOptions, + selectedCard: CardType.TOP_LANGS, + langsCount: 5, + showTitle: false, + }); + + expect(result).toBe( + "/top-langs?username=john&hide_title=true&langs_count=5", + ); + }); + + it("builds pin suffix using userId not selectedUserId", () => { + const result = getFullSuffix({ + ...baseOptions, + selectedCard: CardType.PIN, + showOwner: true, + descriptionLines: 3, + }); + + expect(result).toBe( + "/pin?username=john-github&repo=repo1&show_owner=true&description_lines_count=3", + ); + }); + + it("builds gist suffix", () => { + const result = getFullSuffix({ + ...baseOptions, + selectedCard: CardType.GIST, + showOwner: true, + }); + + expect(result).toBe("/gist?id=gist1&show_owner=true"); + }); + + it("builds wakatime suffix with percent and custom title", () => { + const result = getFullSuffix({ + ...baseOptions, + selectedCard: CardType.WAKATIME, + wakatimeUser: "waka", + usePercent: true, + customTitle: "My Stats", + showTitle: false, + }); + + expect(result).toBe( + "/wakatime?username=waka&hide_title=true&custom_title=My%20Stats&display_format=percent", + ); + }); + + it("adds non-default layouts", () => { + const result = getFullSuffix({ + ...baseOptions, + selectedCard: CardType.TOP_LANGS, + selectedLanguagesLayout: { id: 2, value: "compact", label: "Compact" }, + }); + + expect(result).toBe("/top-langs?username=john&layout=compact"); + }); +}); diff --git a/apps/frontend/src/pages/Home/getFullSuffix.ts b/apps/frontend/src/pages/Home/getFullSuffix.ts new file mode 100644 index 00000000..b333b429 --- /dev/null +++ b/apps/frontend/src/pages/Home/getFullSuffix.ts @@ -0,0 +1,167 @@ +import type { SelectOption } from "../../components/Generic/Select"; +import { DEFAULT_OPTION as LANGUAGES_DEFAULT_LAYOUT } from "../../components/Home/LanguagesLayoutSection"; +import { DEFAULT_OPTION as STATS_DEFAULT_RANK } from "../../components/Home/StatsRankSection"; +import { DEFAULT_OPTION as WAKATIME_DEFAULT_LAYOUT } from "../../components/Home/WakatimeLayoutSection"; +import { CardType } from "../../models/CardType"; + +interface Options { + userId: string; + selectedUserId: string; + selectedCard: CardType; + repo: string; + gist: string; + wakatimeUser: string; + selectedStatsRank: SelectOption; + selectedLanguagesLayout: SelectOption; + selectedWakatimeLayout: SelectOption; + showTitle: boolean; + showOwner: boolean; + descriptionLines: number | undefined; + customTitle: string; + langsCount: number | undefined; + showAllStats: boolean; + showIcons: boolean; + includeAllCommits: boolean; + enableAnimations: boolean; + usePercent: boolean; +} + +export function getFullSuffix({ + userId, + selectedCard, + selectedUserId, + repo, + gist, + wakatimeUser, + selectedStatsRank, + selectedLanguagesLayout, + selectedWakatimeLayout, + showTitle, + showOwner, + descriptionLines, + customTitle, + langsCount, + showAllStats, + showIcons, + includeAllCommits, + enableAnimations, + usePercent, +}: Options): string { + let fullSuffix = `${selectedCard === CardType.STATS ? "" : "/" + selectedCard}?`; + + switch (selectedCard) { + case CardType.STATS: + case CardType.TOP_LANGS: + fullSuffix += `username=${selectedUserId}`; + break; + case CardType.PIN: + /** + * We should use the name of the logged-in user, not the value entered in the + * username field in step 3. + * + * This input is not shown when the PIN card type is selected, + * but it may still contain a different value if the user previously chose another card type. + * + * For example, + * 1. the user could select the STATS card + * 2. enter a username + * 3. then go back and switch to the PIN card, leaving the old value behind. + * + * @see https://github.com/stats-organization/github-stats-extended/pull/73#discussion_r2792177515 + */ + fullSuffix += `username=${userId}&repo=${repo}`; + break; + case CardType.GIST: + fullSuffix += `id=${gist}`; + break; + case CardType.WAKATIME: + fullSuffix += `username=${wakatimeUser}`; + break; + + default: + selectedCard satisfies never; + } + + if ( + selectedStatsRank !== STATS_DEFAULT_RANK && + selectedCard === CardType.STATS + ) { + fullSuffix += `&rank_icon=${selectedStatsRank.value}`; + } + + if ( + selectedLanguagesLayout !== LANGUAGES_DEFAULT_LAYOUT && + selectedCard === CardType.TOP_LANGS + ) { + fullSuffix += `&layout=${selectedLanguagesLayout.value}`; + } + + if ( + selectedWakatimeLayout !== WAKATIME_DEFAULT_LAYOUT && + selectedCard === CardType.WAKATIME + ) { + fullSuffix += `&layout=${selectedWakatimeLayout.value}`; + } + + if ( + !showTitle && + (selectedCard === CardType.STATS || + selectedCard === CardType.TOP_LANGS || + selectedCard === CardType.WAKATIME) + ) { + fullSuffix += "&hide_title=true"; + } + + if ( + showOwner && + (selectedCard === CardType.PIN || selectedCard === CardType.GIST) + ) { + fullSuffix += "&show_owner=true"; + } + + if (descriptionLines && selectedCard === CardType.PIN) { + fullSuffix += `&description_lines_count=${descriptionLines}`; + } + + if ( + customTitle && + (selectedCard === CardType.STATS || selectedCard === CardType.WAKATIME) + ) { + const encodedTitle = encodeURIComponent(customTitle); + fullSuffix += `&custom_title=${encodedTitle}`; + } + + if ( + langsCount && + (selectedCard === CardType.TOP_LANGS || selectedCard === CardType.WAKATIME) + ) { + fullSuffix += `&langs_count=${langsCount}`; + } + + if (showAllStats && selectedCard === CardType.STATS) { + fullSuffix += `&show=reviews,discussions_started,discussions_answered,prs_merged,prs_merged_percentage,prs_commented,prs_reviewed,issues_commented`; + } + + if (showIcons && selectedCard === CardType.STATS) { + fullSuffix += `&show_icons=true`; + } + + if (includeAllCommits && selectedCard === CardType.STATS) { + fullSuffix += `&include_all_commits=true`; + } + + if ( + !enableAnimations && + (selectedCard === CardType.STATS || + selectedCard === CardType.TOP_LANGS || + selectedCard === CardType.WAKATIME) + ) { + fullSuffix += `&disable_animations=${!enableAnimations}`; + } + + if (usePercent && selectedCard === CardType.WAKATIME) { + fullSuffix += `&display_format=percent`; + } + + return fullSuffix; +} diff --git a/apps/frontend/vite.config.ts b/apps/frontend/vite.config.ts index 4f7153a2..2c830a46 100644 --- a/apps/frontend/vite.config.ts +++ b/apps/frontend/vite.config.ts @@ -1,9 +1,9 @@ import path from "node:path"; import react from "@vitejs/plugin-react-swc"; -import { defineConfig } from "vite"; import { nodePolyfills } from "vite-plugin-node-polyfills"; import StringReplace from "vite-plugin-string-replace"; +import { defineConfig } from "vitest/config"; // https://vitejs.dev/config/ export default defineConfig({ @@ -75,4 +75,8 @@ export default defineConfig({ }, ], }, + test: { + dir: "./src", + exclude: ["**/backend/**"], + }, }); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f5c88e64..0371d72d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -4,6 +4,15 @@ settings: autoInstallPeers: true excludeLinksFromLockfile: false +catalogs: + default: + vite: + specifier: 7.3.1 + version: 7.3.1 + vitest: + specifier: 4.0.18 + version: 4.0.18 + importers: .: @@ -206,14 +215,17 @@ importers: specifier: ^3.3.5 version: 3.4.19(yaml@2.8.2) vite: - specifier: 7.3.1 + specifier: catalog:default version: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2) vite-plugin-node-polyfills: specifier: 0.25.0 version: 0.25.0(rollup@4.55.1)(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2)) vite-plugin-string-replace: - specifier: ^1.1.5 + specifier: 1.1.5 version: 1.1.5 + vitest: + specifier: catalog:default + version: 4.0.18(@types/node@25.0.3)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.44.1)(yaml@2.8.2) packages: @@ -1313,6 +1325,12 @@ packages: '@types/babel__traverse@7.28.0': resolution: {integrity: sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==} + '@types/chai@5.2.3': + resolution: {integrity: sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==} + + '@types/deep-eql@4.0.2': + resolution: {integrity: sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==} + '@types/estree@1.0.8': resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} @@ -1543,6 +1561,35 @@ packages: peerDependencies: vite: ^4 || ^5 || ^6 || ^7 + '@vitest/expect@4.0.18': + resolution: {integrity: sha512-8sCWUyckXXYvx4opfzVY03EOiYVxyNrHS5QxX3DAIi5dpJAAkyJezHCP77VMX4HKA2LDT/Jpfo8i2r5BE3GnQQ==} + + '@vitest/mocker@4.0.18': + resolution: {integrity: sha512-HhVd0MDnzzsgevnOWCBj5Otnzobjy5wLBe4EdeeFGv8luMsGcYqDuFRMcttKWZA5vVO8RFjexVovXvAM4JoJDQ==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0-0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.0.18': + resolution: {integrity: sha512-P24GK3GulZWC5tz87ux0m8OADrQIUVDPIjjj65vBXYG17ZeU3qD7r+MNZ1RNv4l8CGU2vtTRqixrOi9fYk/yKw==} + + '@vitest/runner@4.0.18': + resolution: {integrity: sha512-rpk9y12PGa22Jg6g5M3UVVnTS7+zycIGk9ZNGN+m6tZHKQb7jrP7/77WfZy13Y/EUDd52NDsLRQhYKtv7XfPQw==} + + '@vitest/snapshot@4.0.18': + resolution: {integrity: sha512-PCiV0rcl7jKQjbgYqjtakly6T1uwv/5BQ9SwBLekVg/EaYeQFPiXcgrC2Y7vDMA8dM1SUEAEV82kgSQIlXNMvA==} + + '@vitest/spy@4.0.18': + resolution: {integrity: sha512-cbQt3PTSD7P2OARdVW3qWER5EGq7PHlvE+QfzSC0lbwO+xnt7+XH06ZzFjFRgzUX//JmpxrCu92VdwvEPlWSNw==} + + '@vitest/utils@4.0.18': + resolution: {integrity: sha512-msMRKLMVLWygpK3u2Hybgi4MNjcYJvwTb0Ru09+fOyCXIgT5raYP041DRRdiJiI3k/2U6SEbAETB3YtBrUkCFA==} + accepts@2.0.0: resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==} engines: {node: '>= 0.6'} @@ -1660,6 +1707,10 @@ packages: assert@2.1.0: resolution: {integrity: sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + async-function@1.0.0: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} @@ -1842,6 +1893,10 @@ packages: caniuse-lite@1.0.30001767: resolution: {integrity: sha512-34+zUAMhSH+r+9eKmYG+k2Rpt8XttfE4yXAjoZvkAPs15xcYQhyBYdalJ65BzivAvGRMViEjy6oKr/S91loekQ==} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chalk@4.1.2: resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} engines: {node: '>=10'} @@ -2164,6 +2219,9 @@ packages: resolution: {integrity: sha512-BrUQ0cPTB/IwXj23HtwHjS9n7O4h9FX94b4xc5zlTHxeLgTAdzYUDyy6KdExAl9lbN5rtfe44xpjpmj9grxs5w==} engines: {node: '>= 0.4'} + es-module-lexer@1.7.0: + resolution: {integrity: sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==} + es-object-atoms@1.1.1: resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} engines: {node: '>= 0.4'} @@ -2311,6 +2369,9 @@ packages: estree-walker@2.0.2: resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + esutils@2.0.3: resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} engines: {node: '>=0.10.0'} @@ -2337,6 +2398,10 @@ packages: resolution: {integrity: sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==} engines: {node: '>= 0.8.0'} + expect-type@1.3.0: + resolution: {integrity: sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==} + engines: {node: '>=12.0.0'} + expect@30.2.0: resolution: {integrity: sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==} engines: {node: ^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0} @@ -3317,6 +3382,9 @@ packages: resolution: {integrity: sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==} engines: {node: '>= 0.4'} + obug@2.1.1: + resolution: {integrity: sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==} + on-finished@2.4.1: resolution: {integrity: sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==} engines: {node: '>= 0.8'} @@ -3433,6 +3501,9 @@ packages: resolution: {integrity: sha512-Vj7sf++t5pBD637NSfkxpHSMfWaeig5+DKWLhcqIYx6mWQz5hdJTGDVMQiJcw1ZYkhs7AazKDGpRVji1LJCZUQ==} engines: {node: '>=18'} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pbkdf2@3.1.5: resolution: {integrity: sha512-Q3CG/cYvCO1ye4QKkuH7EXxs3VC/rI1/trd+qX2+PolbaKG0H+bgcZzrTt96mMyRtejk+JMCiLUn3y29W8qmFQ==} engines: {node: '>= 0.10'} @@ -3898,6 +3969,9 @@ packages: resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} engines: {node: '>= 0.4'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} @@ -3959,10 +4033,16 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + statuses@2.0.2: resolution: {integrity: sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==} engines: {node: '>= 0.8'} + std-env@3.10.0: + resolution: {integrity: sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==} + stop-iteration-iterator@1.1.0: resolution: {integrity: sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==} engines: {node: '>= 0.4'} @@ -4099,10 +4179,21 @@ packages: resolution: {integrity: sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==} engines: {node: '>=0.6.0'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.0.2: + resolution: {integrity: sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==} + engines: {node: '>=18'} + tinyglobby@0.2.15: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyrainbow@3.0.3: + resolution: {integrity: sha512-PSkbLUoxOFRzJYjjxHJt9xro7D+iilgMX/C9lawzVuYiIdcihh9DXmVibBe8lmcFrRi/VzlPjBxbN7rH24q8/Q==} + engines: {node: '>=14.0.0'} + tldts-core@6.1.86: resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} @@ -4329,6 +4420,40 @@ packages: yaml: optional: true + vitest@4.0.18: + resolution: {integrity: sha512-hOQuK7h0FGKgBAas7v0mSAsnvrIgAvWmRFjmzpJ7SwFHH3g1k2u37JtYwOwmEKhK6ZO3v9ggDBBm0La1LCK4uQ==} + engines: {node: ^20.0.0 || ^22.0.0 || >=24.0.0} + hasBin: true + peerDependencies: + '@edge-runtime/vm': '*' + '@opentelemetry/api': ^1.9.0 + '@types/node': ^20.0.0 || ^22.0.0 || >=24.0.0 + '@vitest/browser-playwright': 4.0.18 + '@vitest/browser-preview': 4.0.18 + '@vitest/browser-webdriverio': 4.0.18 + '@vitest/ui': 4.0.18 + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + '@edge-runtime/vm': + optional: true + '@opentelemetry/api': + optional: true + '@types/node': + optional: true + '@vitest/browser-playwright': + optional: true + '@vitest/browser-preview': + optional: true + '@vitest/browser-webdriverio': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + vm-browserify@1.1.2: resolution: {integrity: sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==} @@ -4381,6 +4506,11 @@ packages: engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + word-wrap@1.2.5: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} @@ -4465,9 +4595,6 @@ packages: peerDependencies: zod: ^3.25.0 || ^4.0.0 - zod@4.3.5: - resolution: {integrity: sha512-k7Nwx6vuWx1IJ9Bjuf4Zt1PEllcwe7cls3VNzm4CQ1/hgtFUK2bRNG3rvnpPUhFjmqJKAKtjV576KnUkHocg/g==} - zod@4.3.6: resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} @@ -5535,6 +5662,13 @@ snapshots: dependencies: '@babel/types': 7.28.5 + '@types/chai@5.2.3': + dependencies: + '@types/deep-eql': 4.0.2 + assertion-error: 2.0.1 + + '@types/deep-eql@4.0.2': {} + '@types/estree@1.0.8': {} '@types/istanbul-lib-coverage@2.0.6': {} @@ -5754,6 +5888,45 @@ snapshots: transitivePeerDependencies: - '@swc/helpers' + '@vitest/expect@4.0.18': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + chai: 6.2.2 + tinyrainbow: 3.0.3 + + '@vitest/mocker@4.0.18(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2))': + dependencies: + '@vitest/spy': 4.0.18 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2) + + '@vitest/pretty-format@4.0.18': + dependencies: + tinyrainbow: 3.0.3 + + '@vitest/runner@4.0.18': + dependencies: + '@vitest/utils': 4.0.18 + pathe: 2.0.3 + + '@vitest/snapshot@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.0.18': {} + + '@vitest/utils@4.0.18': + dependencies: + '@vitest/pretty-format': 4.0.18 + tinyrainbow: 3.0.3 + accepts@2.0.0: dependencies: mime-types: 3.0.2 @@ -5896,6 +6069,8 @@ snapshots: object.assign: 4.1.7 util: 0.12.5 + assertion-error@2.0.1: {} + async-function@1.0.0: {} asynckit@0.4.0: {} @@ -6140,6 +6315,8 @@ snapshots: caniuse-lite@1.0.30001767: {} + chai@6.2.2: {} + chalk@4.1.2: dependencies: ansi-styles: 4.3.0 @@ -6511,6 +6688,8 @@ snapshots: iterator.prototype: 1.1.5 safe-array-concat: 1.1.3 + es-module-lexer@1.7.0: {} + es-object-atoms@1.1.1: dependencies: es-errors: 1.3.0 @@ -6636,8 +6815,8 @@ snapshots: '@babel/parser': 7.28.5 eslint: 9.39.2(jiti@2.6.1) hermes-parser: 0.25.1 - zod: 4.3.5 - zod-validation-error: 4.0.2(zod@4.3.5) + zod: 4.3.6 + zod-validation-error: 4.0.2(zod@4.3.6) transitivePeerDependencies: - supports-color @@ -6741,6 +6920,10 @@ snapshots: estree-walker@2.0.2: {} + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + esutils@2.0.3: {} etag@1.8.1: {} @@ -6768,6 +6951,8 @@ snapshots: exit-x@0.2.2: {} + expect-type@1.3.0: {} + expect@30.2.0: dependencies: '@jest/expect-utils': 30.2.0 @@ -8004,6 +8189,8 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + obug@2.1.1: {} + on-finished@2.4.1: dependencies: ee-first: 1.1.1 @@ -8137,6 +8324,8 @@ snapshots: path-type@6.0.0: {} + pathe@2.0.3: {} + pbkdf2@3.1.5: dependencies: create-hash: 1.2.0 @@ -8651,6 +8840,8 @@ snapshots: side-channel-map: 1.0.1 side-channel-weakmap: 1.0.2 + siginfo@2.0.0: {} + signal-exit@3.0.7: {} signal-exit@4.1.0: {} @@ -8700,8 +8891,12 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + stackback@0.0.2: {} + statuses@2.0.2: {} + std-env@3.10.0: {} + stop-iteration-iterator@1.1.0: dependencies: es-errors: 1.3.0 @@ -8901,11 +9096,17 @@ snapshots: dependencies: setimmediate: 1.0.5 + tinybench@2.9.0: {} + + tinyexec@1.0.2: {} + tinyglobby@0.2.15: dependencies: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyrainbow@3.0.3: {} + tldts-core@6.1.86: {} tldts@6.1.86: @@ -9148,6 +9349,44 @@ snapshots: terser: 5.44.1 yaml: 2.8.2 + vitest@4.0.18(@types/node@25.0.3)(jiti@1.21.7)(jsdom@26.1.0)(terser@5.44.1)(yaml@2.8.2): + dependencies: + '@vitest/expect': 4.0.18 + '@vitest/mocker': 4.0.18(vite@7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2)) + '@vitest/pretty-format': 4.0.18 + '@vitest/runner': 4.0.18 + '@vitest/snapshot': 4.0.18 + '@vitest/spy': 4.0.18 + '@vitest/utils': 4.0.18 + es-module-lexer: 1.7.0 + expect-type: 1.3.0 + magic-string: 0.30.21 + obug: 2.1.1 + pathe: 2.0.3 + picomatch: 4.0.3 + std-env: 3.10.0 + tinybench: 2.9.0 + tinyexec: 1.0.2 + tinyglobby: 0.2.15 + tinyrainbow: 3.0.3 + vite: 7.3.1(@types/node@25.0.3)(jiti@1.21.7)(terser@5.44.1)(yaml@2.8.2) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 25.0.3 + jsdom: 26.1.0 + transitivePeerDependencies: + - jiti + - less + - lightningcss + - msw + - sass + - sass-embedded + - stylus + - sugarss + - terser + - tsx + - yaml + vm-browserify@1.1.2: {} w3c-xmlserializer@5.0.0: @@ -9218,6 +9457,11 @@ snapshots: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + word-wrap@1.2.5: {} wrap-ansi@7.0.0: @@ -9279,10 +9523,8 @@ snapshots: yocto-queue@0.1.0: {} - zod-validation-error@4.0.2(zod@4.3.5): + zod-validation-error@4.0.2(zod@4.3.6): dependencies: - zod: 4.3.5 - - zod@4.3.5: {} + zod: 4.3.6 zod@4.3.6: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8c1413b5..8ba43968 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,7 +1,9 @@ packages: - apps/* -catalog: null +catalog: + vite: 7.3.1 + vitest: 4.0.18 minimumReleaseAge: 10080