From 761f9875b3b34ec61676e125e75e24f6d8f25ffd Mon Sep 17 00:00:00 2001 From: Battlesquid <25509915+Battlesquid@users.noreply.github.com> Date: Mon, 29 Jun 2026 18:11:03 -0700 Subject: [PATCH 1/2] fix: include api base url --- generator.config.ts | 7 +++++-- orval.config.ts | 2 +- src/manual/manual.ts | 20 ++++++++++---------- src/meta/meta.ts | 2 +- src/programs/programs.ts | 4 ++-- src/q-a/q-a.ts | 12 ++++++------ 6 files changed, 25 insertions(+), 22 deletions(-) diff --git a/generator.config.ts b/generator.config.ts index 1d4c5c6..33cbc49 100644 --- a/generator.config.ts +++ b/generator.config.ts @@ -1,4 +1,7 @@ +const BASE_URL = "https://games.recf.org/api/v1"; + export default { - "spec_url": "https://games.recf.org/api/v1/openapi.json", - "output_file": "openapi.json" + base_url: BASE_URL, + spec_url: `${BASE_URL}/openapi.json`, + output_file: "openapi.json" } \ No newline at end of file diff --git a/orval.config.ts b/orval.config.ts index 7514de4..f582f8e 100644 --- a/orval.config.ts +++ b/orval.config.ts @@ -7,7 +7,7 @@ export default defineConfig({ output: { mode: "tags-split", target: "src", - baseUrl: config.spec_url, + baseUrl: config.base_url, client: "fetch", namingConvention: "kebab-case", formatter: "oxfmt", diff --git a/src/manual/manual.ts b/src/manual/manual.ts index 9fc7663..f6d2694 100644 --- a/src/manual/manual.ts +++ b/src/manual/manual.ts @@ -36,7 +36,7 @@ export type getLatestManualResponseError = ( export type getLatestManualResponse = getLatestManualResponseError; export const getGetLatestManualUrl = (slug: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual`; + return `https://games.recf.org/api/v1/programs/${slug}/manual`; }; /** @@ -77,7 +77,7 @@ export type getManualResponseError = getManualResponse404 & { export type getManualResponse = getManualResponseSuccess | getManualResponseError; export const getGetManualUrl = (slug: string, version: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}`; + return `https://games.recf.org/api/v1/programs/${slug}/manual/${version}`; }; /** @@ -119,7 +119,7 @@ export type getManualBundleResponseError = getManualBundleResponse404 & { export type getManualBundleResponse = getManualBundleResponseSuccess | getManualBundleResponseError; export const getGetManualBundleUrl = (slug: string, version: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/bundle`; + return `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/bundle`; }; /** @@ -161,7 +161,7 @@ export type getChangelogResponseError = getChangelogResponse404 & { export type getChangelogResponse = getChangelogResponseSuccess | getChangelogResponseError; export const getGetChangelogUrl = (slug: string, version: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/changelog`; + return `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/changelog`; }; /** @@ -214,8 +214,8 @@ export const getSearchManualUrl = (slug: string, version: string, params?: Searc const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/search?${stringifiedParams}` - : `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/search`; + ? `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/search?${stringifiedParams}` + : `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/search`; }; /** @@ -273,8 +273,8 @@ export const getExportManualPdfUrl = ( const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/export.pdf?${stringifiedParams}` - : `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/export.pdf`; + ? `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/export.pdf?${stringifiedParams}` + : `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/export.pdf`; }; /** @@ -316,7 +316,7 @@ export type getRuleResponseError = getRuleResponse404 & { export type getRuleResponse = getRuleResponseSuccess | getRuleResponseError; export const getGetRuleUrl = (slug: string, version: string, stableKey: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/rules/${stableKey}`; + return `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/rules/${stableKey}`; }; /** @@ -359,7 +359,7 @@ export type getDefinitionResponseError = getDefinitionResponse404 & { export type getDefinitionResponse = getDefinitionResponseSuccess | getDefinitionResponseError; export const getGetDefinitionUrl = (slug: string, version: string, stableKey: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/manual/${version}/definitions/${stableKey}`; + return `https://games.recf.org/api/v1/programs/${slug}/manual/${version}/definitions/${stableKey}`; }; /** diff --git a/src/meta/meta.ts b/src/meta/meta.ts index 0afa613..cbea272 100644 --- a/src/meta/meta.ts +++ b/src/meta/meta.ts @@ -19,7 +19,7 @@ export type getOpenApiSpecResponseSuccess = getOpenApiSpecResponse200 & { export type getOpenApiSpecResponse = getOpenApiSpecResponseSuccess; export const getGetOpenApiSpecUrl = () => { - return `https://games.recf.org/api/v1/openapi.json/openapi.json`; + return `https://games.recf.org/api/v1/openapi.json`; }; /** diff --git a/src/programs/programs.ts b/src/programs/programs.ts index b257146..8b4697b 100644 --- a/src/programs/programs.ts +++ b/src/programs/programs.ts @@ -19,7 +19,7 @@ export type listProgramsResponseSuccess = listProgramsResponse200 & { export type listProgramsResponse = listProgramsResponseSuccess; export const getListProgramsUrl = () => { - return `https://games.recf.org/api/v1/openapi.json/programs`; + return `https://games.recf.org/api/v1/programs`; }; /** @@ -57,7 +57,7 @@ export type getProgramResponseError = getProgramResponse404 & { export type getProgramResponse = getProgramResponseSuccess | getProgramResponseError; export const getGetProgramUrl = (slug: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}`; + return `https://games.recf.org/api/v1/programs/${slug}`; }; /** diff --git a/src/q-a/q-a.ts b/src/q-a/q-a.ts index 4c67c53..54e20fa 100644 --- a/src/q-a/q-a.ts +++ b/src/q-a/q-a.ts @@ -45,8 +45,8 @@ export const getListQaUrl = (slug: string, params?: ListQaParams) => { const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `https://games.recf.org/api/v1/openapi.json/programs/${slug}/qa?${stringifiedParams}` - : `https://games.recf.org/api/v1/openapi.json/programs/${slug}/qa`; + ? `https://games.recf.org/api/v1/programs/${slug}/qa?${stringifiedParams}` + : `https://games.recf.org/api/v1/programs/${slug}/qa`; }; /** @@ -88,7 +88,7 @@ export type getQaResponseError = getQaResponse404 & { export type getQaResponse = getQaResponseSuccess | getQaResponseError; export const getGetQaUrl = (slug: string, qnaNumber: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/qa/${qnaNumber}`; + return `https://games.recf.org/api/v1/programs/${slug}/qa/${qnaNumber}`; }; /** @@ -130,7 +130,7 @@ export type getQaRssResponseError = getQaRssResponse404 & { export type getQaRssResponse = getQaRssResponseSuccess | getQaRssResponseError; export const getGetQaRssUrl = (slug: string) => { - return `https://games.recf.org/api/v1/openapi.json/programs/${slug}/qa.rss`; + return `https://games.recf.org/api/v1/programs/${slug}/qa.rss`; }; /** @@ -184,8 +184,8 @@ export const getListRuleQaUrl = (slug: string, stableKey: string, params?: ListR const stringifiedParams = normalizedParams.toString(); return stringifiedParams.length > 0 - ? `https://games.recf.org/api/v1/openapi.json/programs/${slug}/rules/${stableKey}/qa?${stringifiedParams}` - : `https://games.recf.org/api/v1/openapi.json/programs/${slug}/rules/${stableKey}/qa`; + ? `https://games.recf.org/api/v1/programs/${slug}/rules/${stableKey}/qa?${stringifiedParams}` + : `https://games.recf.org/api/v1/programs/${slug}/rules/${stableKey}/qa`; }; /** From 094752f04abfdc7f6aa6a159aa3f89a40010fa3b Mon Sep 17 00:00:00 2001 From: Battlesquid <25509915+Battlesquid@users.noreply.github.com> Date: Mon, 29 Jun 2026 20:14:42 -0700 Subject: [PATCH 2/2] feat: add tests --- package.json | 15 +- pnpm-lock.yaml | 706 +++++++++++++++++++++++++++++++++++++++++ tests/helpers.ts | 16 + tests/manual.test.ts | 206 ++++++++++++ tests/meta.test.ts | 37 +++ tests/programs.test.ts | 70 ++++ tests/q-a.test.ts | 171 ++++++++++ tsconfig.json | 1 - vitest.config.ts | 8 + 9 files changed, 1224 insertions(+), 6 deletions(-) create mode 100644 tests/helpers.ts create mode 100644 tests/manual.test.ts create mode 100644 tests/meta.test.ts create mode 100644 tests/programs.test.ts create mode 100644 tests/q-a.test.ts create mode 100644 vitest.config.ts diff --git a/package.json b/package.json index 15ce34e..7d45000 100644 --- a/package.json +++ b/package.json @@ -9,17 +9,20 @@ "scripts": { "download_spec": "tsx scripts/fetch-openapi.ts", "generate": "pnpm download_spec && orval", - "generate:clean": "orval --clean && pnpm generate", - "clean": "rm -rf dist openapi.json", + "generate:clean": "orval --clean && rm openapi.json pnpm generate", + "clean": "rm -rf dist", "build": "tsc", - "build:clean": "pnpm clean && pnpm build" + "build:clean": "pnpm clean && pnpm build", + "test": "vitest run", + "test:watch": "vitest" }, "devDependencies": { "@types/node": "^26.0.1", "orval": "^8.19.0", "oxfmt": "^0.56.0", "tsx": "^4.22.4", - "typescript": "^6.0.3" + "typescript": "^6.0.3", + "vitest": "^4.1.9" }, "volta": { "node": "26.4.0", @@ -33,5 +36,7 @@ "api-client", "typescript" ], - "files": ["dist"] + "files": [ + "dist" + ] } \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 16dd865..cd4613a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,6 +23,9 @@ importers: typescript: specifier: ^6.0.3 version: 6.0.3 + vitest: + specifier: ^4.1.9 + version: 4.1.9(@types/node@26.0.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) packages: @@ -31,6 +34,15 @@ packages: peerDependencies: commander: ~15.0.0 + '@emnapi/core@1.11.1': + resolution: {integrity: sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==} + + '@emnapi/runtime@1.11.1': + resolution: {integrity: sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==} + + '@emnapi/wasi-threads@1.2.2': + resolution: {integrity: sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==} + '@esbuild/aix-ppc64@0.28.1': resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==} engines: {node: '>=18'} @@ -190,6 +202,15 @@ packages: '@gerrit0/mini-shiki@3.23.0': resolution: {integrity: sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==} + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@napi-rs/wasm-runtime@1.1.6': + resolution: {integrity: sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + '@orval/angular@8.19.0': resolution: {integrity: sha512-w3sBRPCVmZDlpbSGyBhVsrleMsrD63AzxYOFfLx+I/Wy6FxN1xartlOUGGyroQMFLDx+Lk74+olQQp6IVnRerg==} @@ -236,6 +257,9 @@ packages: '@orval/zod@8.19.0': resolution: {integrity: sha512-8o2MZ3eGzQMRz2j0C4YvL8J+e1lYgA6oo6aTiizoqrgmXKLdMqFaoz1SWB0UoEYCURmhPEohfrGm3J1+SRcI9Q==} + '@oxc-project/types@0.137.0': + resolution: {integrity: sha512-WT+Gb24i8hmvo85AIv2oEYouEXkRlKAlT9WaCa3TfLgNCN+GhrJOGZuIlMouAh38Qe4QOx26eUOVsq70qXrywA==} + '@oxfmt/binding-android-arm-eabi@0.56.0': resolution: {integrity: sha512-CSCxi7ovYojgfdPOdUb9T508HKeAdDIKeRGg7x8IZwVJrWz9gVgX7MbUnFqtQAE4QvoNo07mj2JlwnOzJw4qqA==} engines: {node: ^20.19.0 || >=22.12.0} @@ -358,6 +382,104 @@ packages: cpu: [x64] os: [win32] + '@rolldown/binding-android-arm64@1.1.3': + resolution: {integrity: sha512-DT6Z3PhvioeHMvxo+xHc3KtqggrI7CCTXCmC2h/5zUlp5jVitv7XEy+9q5/7v8IolhlioawpMo8Kg0EEBy7J0g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.1.3': + resolution: {integrity: sha512-0NwgwsjM7LrsuVnXMK3koTpagBNOhloc/BNjKqZjv4V5zI5r13qx69uVhRx+o5Z0yy4Hzq+lpy7TAgUG/ocvrw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.1.3': + resolution: {integrity: sha512-YtiBp4disu6V560loT6PjMdiRaWmVvDNrUunAalbiFx2ggeJwxdAsgZMcoGP17uyAsTwAj5V1niksxlHnVQ1Sw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.1.3': + resolution: {integrity: sha512-yD3EkEdXk2LypPxnf/kSZHirarsI8gcPzc62SukhR9VJTyvV+F9Q/GxWNuCojc7sXyuVC4DxRGhdDK4X8VSsbw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + resolution: {integrity: sha512-c+8vieQbsD7HNAHKIA34w0GJ9FedFFuJGD+7E6vz7Q3uqAIugL5p45fhlsj4UaAsHpcmlqugBWMhA0/j7o0sIg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + resolution: {integrity: sha512-50jD0uUwLvur7Zz9LHz17kaAdTPjn5wN93hEgjvmYFRZwiR7ZJYovTd5ipyWJDAnXKvZ+wgc+/Ika6dwSF5OcA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.1.3': + resolution: {integrity: sha512-BO9+oPL8K9poZJBfYPsXNtYjPE5uM3qeehT3aFcW4LITOl+iSqhp0abzjR2nWBUNjIZeKXjAEWBZ64WjNoHd6w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + resolution: {integrity: sha512-f3VpLB1vQ0Eo6ecr/6cekLnvYMFF4YBFoVGkfkvPLq1bAkbAwHYQPZKoAmG6OJyTcxxoC+AvezGx/S1obNC0Mw==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + resolution: {integrity: sha512-AmurZ26Pqx/RI9N1gzEOCklkKXl927yjfXWUUS0O7Puh8ARM/Ob8qfrD3qnWksScdw6cSrW5PSHE9DyLu7+PtA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.1.3': + resolution: {integrity: sha512-JJpqs8bRGITDOdbkNKnlojzBabbOHrqjSvDr0IVsZObE1lBcPjxItUEY9eWIDbxaJ3cGrXPWGfGkIxFijg/URg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.1.3': + resolution: {integrity: sha512-rSJcdjPxzA/by/6/rYs+v+bXU7UjvnbUWz8MJb6kh6+knqB1dCrtHg0uu7C/4haqJvqdkYHQ5IGn+tCH9GLW/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.1.3': + resolution: {integrity: sha512-hQ3/PYkDJICgevvyNcVrihVeqq7k1Pp3VZ9lY+dauAYUJKO+auqApvANhvR1An9BhmqYKvW2Mu1F9u4DXSMLxQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.1.3': + resolution: {integrity: sha512-Elcv/BtML9lXrV6JuKITc/grN2kYV9gjsQpW8Jfw4ioK0TOkjBjye0nnyqQNy9STNaI20lXNaQBRrD5gSgR0Yg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + resolution: {integrity: sha512-2DrEfhluH9yhiaFApmsjsjwrSYbNcY1oFTzYSP1a535jDbV98zCFanA/96TBUd0iDFcxGmw9QRExwGCXz3U+/g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.1.3': + resolution: {integrity: sha512-OL4OMk7UPXOeVGGd3qo5zJyPIljf4AFgk5QAkPPS+OoLuOOozhuaQGC18MxVTnw/06q93gShAJzlwnSCY9YtqA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.1': + resolution: {integrity: sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==} + '@scalar/helpers@0.9.0': resolution: {integrity: sha512-M34CLRCttqC1bXthI/QSzQj0s5C6nrU2PFWf/vOT3RpycbiGDGQbqR+5RfFzpOIQvRqbHfNdcRbeiZBw+vCbkQ==} engines: {node: '>=22'} @@ -404,6 +526,21 @@ packages: resolution: {integrity: sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==} engines: {node: '>=18'} + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@tybys/wasm-util@0.10.3': + resolution: {integrity: sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==} + + '@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.9': + resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/hast@3.0.4': resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} @@ -413,6 +550,35 @@ packages: '@types/unist@3.0.3': resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + '@vitest/expect@4.1.9': + resolution: {integrity: sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==} + + '@vitest/mocker@4.1.9': + resolution: {integrity: sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==} + peerDependencies: + msw: ^2.4.9 + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + peerDependenciesMeta: + msw: + optional: true + vite: + optional: true + + '@vitest/pretty-format@4.1.9': + resolution: {integrity: sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==} + + '@vitest/runner@4.1.9': + resolution: {integrity: sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==} + + '@vitest/snapshot@4.1.9': + resolution: {integrity: sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==} + + '@vitest/spy@4.1.9': + resolution: {integrity: sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==} + + '@vitest/utils@4.1.9': + resolution: {integrity: sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==} + acorn@8.17.0: resolution: {integrity: sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg==} engines: {node: '>=0.4.0'} @@ -440,6 +606,10 @@ packages: argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + assertion-error@2.0.1: + resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} + engines: {node: '>=12'} + balanced-match@4.0.4: resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} engines: {node: 18 || 20 || >=22} @@ -448,6 +618,10 @@ packages: resolution: {integrity: sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==} engines: {node: 18 || 20 || >=22} + chai@6.2.2: + resolution: {integrity: sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==} + engines: {node: '>=18'} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -459,6 +633,9 @@ packages: compare-versions@6.1.1: resolution: {integrity: sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg==} + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + cross-spawn@7.0.6: resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} engines: {node: '>= 8'} @@ -472,15 +649,25 @@ packages: supports-color: optional: true + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + entities@4.5.0: resolution: {integrity: sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==} engines: {node: '>=0.12'} + es-module-lexer@2.1.0: + resolution: {integrity: sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==} + esbuild@0.28.1: resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==} engines: {node: '>=18'} hasBin: true + 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'} @@ -489,6 +676,10 @@ packages: resolution: {integrity: sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==} engines: {node: ^18.19.0 || >=20.5.0} + expect-type@1.4.0: + resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} + engines: {node: '>=12.0.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -572,6 +763,80 @@ packages: resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + linkify-it@5.0.1: resolution: {integrity: sha512-wVoTjP4Q6R0NW5hiZkVJaFZPWgtXfoGF+6LucL3/FtiNjmcHhYjEr5f1Kqjirc1nBW07J/ZuRFumqr2oqccEWg==} @@ -582,6 +847,9 @@ packages: lunr@2.3.9: resolution: {integrity: sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==} + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + markdown-it@14.2.0: resolution: {integrity: sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==} hasBin: true @@ -596,10 +864,19 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + nanoid@3.3.15: + resolution: {integrity: sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + npm-run-path@6.0.0: resolution: {integrity: sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==} engines: {node: '>=18'} + obug@2.1.3: + resolution: {integrity: sha512-9miFgM2OFba7hB+pRgvtV84pYTBaoTHohvmIgiRt6dRIzbwEOIaNaP+dIlGs2fNFoB0SeISs0Jz5WFVRid6Xyg==} + engines: {node: '>=12.20.0'} + orval@8.19.0: resolution: {integrity: sha512-P/DFRVm4F/NZVg42x3K2/iUxPl3EWvuBLLxUPdxVqm2tOg7fAD1mqrigBpd3cg1DqXhN1PsD8bktLH8BMLh1Wg==} engines: {node: '>=22.18.0'} @@ -646,10 +923,17 @@ packages: pathe@2.0.3: resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + picomatch@4.0.4: resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} engines: {node: '>=12'} + postcss@8.5.16: + resolution: {integrity: sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==} + engines: {node: ^10 || ^12 || >=14} + pretty-ms@9.3.0: resolution: {integrity: sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==} engines: {node: '>=18'} @@ -673,6 +957,11 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rolldown@1.1.3: + resolution: {integrity: sha512-1F1eEtUBtFvcGm1HQ9TiUIUHPQG7mSAODrhIzjxoUEFuo8OcbrGLiVLkevNgj84TE4lnHvnumwFjhJO5Eu135g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -681,10 +970,23 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + siginfo@2.0.0: + resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} engines: {node: '>=14'} + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + stackback@0.0.2: + resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} + + std-env@4.1.0: + resolution: {integrity: sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==} + string-argv@0.3.2: resolution: {integrity: sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q==} engines: {node: '>=0.6.19'} @@ -693,6 +995,13 @@ packages: resolution: {integrity: sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==} engines: {node: '>=18'} + tinybench@2.9.0: + resolution: {integrity: sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==} + + tinyexec@1.2.4: + resolution: {integrity: sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==} + engines: {node: '>=18'} + tinyglobby@0.2.17: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} @@ -701,6 +1010,13 @@ packages: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + tinyrainbow@3.1.0: + resolution: {integrity: sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==} + engines: {node: '>=14.0.0'} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.22.4: resolution: {integrity: sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==} engines: {node: '>=18.0.0'} @@ -744,11 +1060,100 @@ packages: resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} engines: {node: '>= 10.0.0'} + vite@8.1.0: + resolution: {integrity: sha512-BuJcQK/56NQTWDGn4ABea3q4SSBdNPWwNZKTkkUpcMPnLoquSYH8llRtSUIgoL1KSCpHt5eghLShn50mH36y7Q==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.3.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + vitest@4.1.9: + resolution: {integrity: sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==} + 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.1.9 + '@vitest/browser-preview': 4.1.9 + '@vitest/browser-webdriverio': 4.1.9 + '@vitest/coverage-istanbul': 4.1.9 + '@vitest/coverage-v8': 4.1.9 + '@vitest/ui': 4.1.9 + happy-dom: '*' + jsdom: '*' + vite: ^6.0.0 || ^7.0.0 || ^8.0.0 + 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/coverage-istanbul': + optional: true + '@vitest/coverage-v8': + optional: true + '@vitest/ui': + optional: true + happy-dom: + optional: true + jsdom: + optional: true + which@2.0.2: resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} engines: {node: '>= 8'} hasBin: true + why-is-node-running@2.3.0: + resolution: {integrity: sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==} + engines: {node: '>=8'} + hasBin: true + yaml@2.9.0: resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} engines: {node: '>= 14.6'} @@ -768,6 +1173,22 @@ snapshots: dependencies: commander: 15.0.0 + '@emnapi/core@1.11.1': + dependencies: + '@emnapi/wasi-threads': 1.2.2 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.11.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.2': + dependencies: + tslib: 2.8.1 + optional: true + '@esbuild/aix-ppc64@0.28.1': optional: true @@ -854,6 +1275,15 @@ snapshots: '@shikijs/types': 3.23.0 '@shikijs/vscode-textmate': 10.0.2 + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@napi-rs/wasm-runtime@1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1)': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@tybys/wasm-util': 0.10.3 + optional: true + '@orval/angular@8.19.0(typescript@6.0.3)': dependencies: '@orval/core': 8.19.0(typescript@6.0.3) @@ -970,6 +1400,8 @@ snapshots: - supports-color - typescript + '@oxc-project/types@0.137.0': {} + '@oxfmt/binding-android-arm-eabi@0.56.0': optional: true @@ -1027,6 +1459,57 @@ snapshots: '@oxfmt/binding-win32-x64-msvc@0.56.0': optional: true + '@rolldown/binding-android-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-arm64@1.1.3': + optional: true + + '@rolldown/binding-darwin-x64@1.1.3': + optional: true + + '@rolldown/binding-freebsd-x64@1.1.3': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.1.3': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.1.3': + optional: true + + '@rolldown/binding-linux-x64-musl@1.1.3': + optional: true + + '@rolldown/binding-openharmony-arm64@1.1.3': + optional: true + + '@rolldown/binding-wasm32-wasi@1.1.3': + dependencies: + '@emnapi/core': 1.11.1 + '@emnapi/runtime': 1.11.1 + '@napi-rs/wasm-runtime': 1.1.6(@emnapi/core@1.11.1)(@emnapi/runtime@1.11.1) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.1.3': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.1.3': + optional: true + + '@rolldown/pluginutils@1.0.1': {} + '@scalar/helpers@0.9.0': {} '@scalar/json-magic@0.12.17': @@ -1080,6 +1563,22 @@ snapshots: '@sindresorhus/merge-streams@4.0.0': {} + '@standard-schema/spec@1.1.0': {} + + '@tybys/wasm-util@0.10.3': + dependencies: + tslib: 2.8.1 + optional: true + + '@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.9': {} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 @@ -1090,6 +1589,47 @@ snapshots: '@types/unist@3.0.3': {} + '@vitest/expect@4.1.9': + dependencies: + '@standard-schema/spec': 1.1.0 + '@types/chai': 5.2.3 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + chai: 6.2.2 + tinyrainbow: 3.1.0 + + '@vitest/mocker@4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0))': + dependencies: + '@vitest/spy': 4.1.9 + estree-walker: 3.0.3 + magic-string: 0.30.21 + optionalDependencies: + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + + '@vitest/pretty-format@4.1.9': + dependencies: + tinyrainbow: 3.1.0 + + '@vitest/runner@4.1.9': + dependencies: + '@vitest/utils': 4.1.9 + pathe: 2.0.3 + + '@vitest/snapshot@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + '@vitest/utils': 4.1.9 + magic-string: 0.30.21 + pathe: 2.0.3 + + '@vitest/spy@4.1.9': {} + + '@vitest/utils@4.1.9': + dependencies: + '@vitest/pretty-format': 4.1.9 + convert-source-map: 2.0.0 + tinyrainbow: 3.1.0 + acorn@8.17.0: {} ajv-draft-04@1.0.0(ajv@8.20.0): @@ -1109,12 +1649,16 @@ snapshots: argparse@2.0.1: {} + assertion-error@2.0.1: {} + balanced-match@4.0.4: {} brace-expansion@5.0.6: dependencies: balanced-match: 4.0.4 + chai@6.2.2: {} + chokidar@5.0.0: dependencies: readdirp: 5.0.0 @@ -1123,6 +1667,8 @@ snapshots: compare-versions@6.1.1: {} + convert-source-map@2.0.0: {} + cross-spawn@7.0.6: dependencies: path-key: 3.1.1 @@ -1133,8 +1679,12 @@ snapshots: dependencies: ms: 2.1.3 + detect-libc@2.1.2: {} + entities@4.5.0: {} + es-module-lexer@2.1.0: {} + esbuild@0.28.1: optionalDependencies: '@esbuild/aix-ppc64': 0.28.1 @@ -1164,6 +1714,10 @@ snapshots: '@esbuild/win32-ia32': 0.28.1 '@esbuild/win32-x64': 0.28.1 + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.9 + esutils@2.0.3: {} execa@9.6.1: @@ -1181,6 +1735,8 @@ snapshots: strip-final-newline: 4.0.0 yoctocolors: 2.1.2 + expect-type@1.4.0: {} + fast-deep-equal@3.1.3: {} fast-uri@3.1.2: {} @@ -1246,6 +1802,55 @@ snapshots: leven@4.1.0: {} + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + linkify-it@5.0.1: dependencies: uc.micro: 2.1.0 @@ -1256,6 +1861,10 @@ snapshots: lunr@2.3.9: {} + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + markdown-it@14.2.0: dependencies: argparse: 2.0.1 @@ -1273,11 +1882,15 @@ snapshots: ms@2.1.3: {} + nanoid@3.3.15: {} + npm-run-path@6.0.0: dependencies: path-key: 4.0.0 unicorn-magic: 0.3.0 + obug@2.1.3: {} + orval@8.19.0(typescript@6.0.3): dependencies: '@commander-js/extra-typings': 15.0.0(commander@15.0.0) @@ -1354,8 +1967,16 @@ snapshots: pathe@2.0.3: {} + picocolors@1.1.1: {} + picomatch@4.0.4: {} + postcss@8.5.16: + dependencies: + nanoid: 3.3.15 + picocolors: 1.1.1 + source-map-js: 1.2.1 + pretty-ms@9.3.0: dependencies: parse-ms: 4.0.0 @@ -1370,18 +1991,51 @@ snapshots: resolve-pkg-maps@1.0.0: {} + rolldown@1.1.3: + dependencies: + '@oxc-project/types': 0.137.0 + '@rolldown/pluginutils': 1.0.1 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.1.3 + '@rolldown/binding-darwin-arm64': 1.1.3 + '@rolldown/binding-darwin-x64': 1.1.3 + '@rolldown/binding-freebsd-x64': 1.1.3 + '@rolldown/binding-linux-arm-gnueabihf': 1.1.3 + '@rolldown/binding-linux-arm64-gnu': 1.1.3 + '@rolldown/binding-linux-arm64-musl': 1.1.3 + '@rolldown/binding-linux-ppc64-gnu': 1.1.3 + '@rolldown/binding-linux-s390x-gnu': 1.1.3 + '@rolldown/binding-linux-x64-gnu': 1.1.3 + '@rolldown/binding-linux-x64-musl': 1.1.3 + '@rolldown/binding-openharmony-arm64': 1.1.3 + '@rolldown/binding-wasm32-wasi': 1.1.3 + '@rolldown/binding-win32-arm64-msvc': 1.1.3 + '@rolldown/binding-win32-x64-msvc': 1.1.3 + shebang-command@2.0.0: dependencies: shebang-regex: 3.0.0 shebang-regex@3.0.0: {} + siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} + + stackback@0.0.2: {} + + std-env@4.1.0: {} + string-argv@0.3.2: {} strip-final-newline@4.0.0: {} + tinybench@2.9.0: {} + + tinyexec@1.2.4: {} + tinyglobby@0.2.17: dependencies: fdir: 6.5.0(picomatch@4.0.4) @@ -1389,6 +2043,11 @@ snapshots: tinypool@2.1.0: {} + tinyrainbow@3.1.0: {} + + tslib@2.8.1: + optional: true + tsx@4.22.4: dependencies: esbuild: 0.28.1 @@ -1422,10 +2081,57 @@ snapshots: universalify@2.0.1: {} + vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.16 + rolldown: 1.1.3 + tinyglobby: 0.2.17 + optionalDependencies: + '@types/node': 26.0.1 + esbuild: 0.28.1 + fsevents: 2.3.3 + jiti: 2.7.0 + tsx: 4.22.4 + yaml: 2.9.0 + + vitest@4.1.9(@types/node@26.0.1)(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)): + dependencies: + '@vitest/expect': 4.1.9 + '@vitest/mocker': 4.1.9(vite@8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0)) + '@vitest/pretty-format': 4.1.9 + '@vitest/runner': 4.1.9 + '@vitest/snapshot': 4.1.9 + '@vitest/spy': 4.1.9 + '@vitest/utils': 4.1.9 + es-module-lexer: 2.1.0 + expect-type: 1.4.0 + magic-string: 0.30.21 + obug: 2.1.3 + pathe: 2.0.3 + picomatch: 4.0.4 + std-env: 4.1.0 + tinybench: 2.9.0 + tinyexec: 1.2.4 + tinyglobby: 0.2.17 + tinyrainbow: 3.1.0 + vite: 8.1.0(@types/node@26.0.1)(esbuild@0.28.1)(jiti@2.7.0)(tsx@4.22.4)(yaml@2.9.0) + why-is-node-running: 2.3.0 + optionalDependencies: + '@types/node': 26.0.1 + transitivePeerDependencies: + - msw + which@2.0.2: dependencies: isexe: 2.0.0 + why-is-node-running@2.3.0: + dependencies: + siginfo: 2.0.0 + stackback: 0.0.2 + yaml@2.9.0: {} yocto-queue@1.2.2: {} diff --git a/tests/helpers.ts b/tests/helpers.ts new file mode 100644 index 0000000..532bc65 --- /dev/null +++ b/tests/helpers.ts @@ -0,0 +1,16 @@ +import { vi } from "vitest"; + +export function makeMockResponse( + status: number, + body: unknown, + contentType = "application/json", +) { + const headers = new Headers({ "content-type": contentType }); + const bodyString = typeof body === "string" ? body : JSON.stringify(body); + return { + status, + headers, + text: vi.fn().mockResolvedValue(bodyString), + blob: vi.fn().mockResolvedValue(new Blob([bodyString])), + }; +} diff --git a/tests/manual.test.ts b/tests/manual.test.ts new file mode 100644 index 0000000..0168fbd --- /dev/null +++ b/tests/manual.test.ts @@ -0,0 +1,206 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + exportManualPdf, + getChangelog, + getDefinition, + getExportManualPdfUrl, + getGetChangelogUrl, + getGetDefinitionUrl, + getGetLatestManualUrl, + getGetManualBundleUrl, + getGetManualUrl, + getGetRuleUrl, + getManual, + getRule, + getSearchManualUrl, +} from "../src/manual/manual"; +import { makeMockResponse } from "./helpers"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("URL builders", () => { + it("getGetLatestManualUrl", () => { + expect(getGetLatestManualUrl("achieve")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual", + ); + }); + + it("getGetManualUrl", () => { + expect(getGetManualUrl("achieve", "1.0")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0", + ); + }); + + it("getGetManualBundleUrl", () => { + expect(getGetManualBundleUrl("achieve", "1.0")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/bundle", + ); + }); + + it("getGetChangelogUrl", () => { + expect(getGetChangelogUrl("achieve", "1.0")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/changelog", + ); + }); + + it("getSearchManualUrl without params", () => { + expect(getSearchManualUrl("achieve", "1.0")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/search", + ); + }); + + it("getSearchManualUrl with params", () => { + const url = getSearchManualUrl("achieve", "1.0", { q: "robot skills" }); + expect(url).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/search?q=robot+skills", + ); + }); + + it("getSearchManualUrl skips undefined params", () => { + const url = getSearchManualUrl("achieve", "1.0", {}); + expect(url).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/search", + ); + }); + + it("getExportManualPdfUrl without params", () => { + expect(getExportManualPdfUrl("achieve", "1.0")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/export.pdf", + ); + }); + + it("getExportManualPdfUrl with params", () => { + const url = getExportManualPdfUrl("achieve", "1.0", { includeQa: true }); + expect(url).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/export.pdf?includeQa=true", + ); + }); + + it("getGetRuleUrl", () => { + expect(getGetRuleUrl("achieve", "1.0", "R1")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/rules/R1", + ); + }); + + it("getGetDefinitionUrl", () => { + expect(getGetDefinitionUrl("achieve", "1.0", "D1")).toBe( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0/definitions/D1", + ); + }); +}); + +describe("getManual", () => { + it("returns parsed data and status 200 on success", async () => { + const mockData = { data: { versionLabel: "1.0" }, meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await getManual("achieve", "1.0"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("returns parsed error and status 404", async () => { + const mockError = { message: "Not found" }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(404, mockError))); + + const result = await getManual("achieve", "1.0"); + + expect(result.status).toBe(404); + expect(result.data).toEqual(mockError); + }); + + it("calls fetch with correct URL and GET method", async () => { + const mockFetch = vi.fn().mockResolvedValue(makeMockResponse(200, {})); + vi.stubGlobal("fetch", mockFetch); + + await getManual("achieve", "1.0"); + + expect(mockFetch).toHaveBeenCalledWith( + "https://games.recf.org/api/v1/programs/achieve/manual/1.0", + expect.objectContaining({ method: "GET" }), + ); + }); + + it("passes through request options", async () => { + const mockFetch = vi.fn().mockResolvedValue(makeMockResponse(200, {})); + vi.stubGlobal("fetch", mockFetch); + + await getManual("achieve", "1.0", { headers: { "x-custom": "value" } }); + + expect(mockFetch).toHaveBeenCalledWith( + expect.any(String), + expect.objectContaining({ headers: { "x-custom": "value" } }), + ); + }); +}); + +describe("getChangelog", () => { + it("returns empty data for 304 (no-body) response", async () => { + const mockRes = makeMockResponse(304, {}); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes)); + + const result = await getChangelog("achieve", "1.0"); + + expect(result.status).toBe(304); + expect(mockRes.text).not.toHaveBeenCalled(); + }); +}); + +describe("exportManualPdf", () => { + it("calls res.blob() instead of res.text()", async () => { + const mockRes = makeMockResponse(200, ""); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes)); + + await exportManualPdf("achieve", "1.0"); + + expect(mockRes.blob).toHaveBeenCalled(); + expect(mockRes.text).not.toHaveBeenCalled(); + }); + + it("returns status 200 with blob data", async () => { + const mockRes = makeMockResponse(200, ""); + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(mockRes)); + + const result = await exportManualPdf("achieve", "1.0"); + + expect(result.status).toBe(200); + expect(result.data).toBeInstanceOf(Blob); + }); +}); + +describe("getRule", () => { + it("returns parsed rule on success", async () => { + const mockData = { data: { key: "R1", content: "Rule content" }, meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await getRule("achieve", "1.0", "R1"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); +}); + +describe("getDefinition", () => { + it("returns parsed definition on success", async () => { + const mockData = { data: { key: "D1", content: "Definition content" }, meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await getDefinition("achieve", "1.0", "D1"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("returns error data on 404", async () => { + const mockError = { message: "Not found" }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(404, mockError))); + + const result = await getDefinition("achieve", "1.0", "D1"); + + expect(result.status).toBe(404); + expect(result.data).toEqual(mockError); + }); +}); diff --git a/tests/meta.test.ts b/tests/meta.test.ts new file mode 100644 index 0000000..09b81ad --- /dev/null +++ b/tests/meta.test.ts @@ -0,0 +1,37 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { getGetOpenApiSpecUrl, getOpenApiSpec } from "../src/meta/meta"; +import { makeMockResponse } from "./helpers"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("URL builders", () => { + it("getGetOpenApiSpecUrl", () => { + expect(getGetOpenApiSpecUrl()).toBe("https://games.recf.org/api/v1/openapi.json"); + }); +}); + +describe("getOpenApiSpec", () => { + it("returns parsed spec on success", async () => { + const mockSpec = { openapi: "3.1.0", info: { title: "RECF API" } }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockSpec))); + + const result = await getOpenApiSpec(); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockSpec); + }); + + it("calls fetch with correct URL and GET method", async () => { + const mockFetch = vi.fn().mockResolvedValue(makeMockResponse(200, {})); + vi.stubGlobal("fetch", mockFetch); + + await getOpenApiSpec(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://games.recf.org/api/v1/openapi.json", + expect.objectContaining({ method: "GET" }), + ); + }); +}); diff --git a/tests/programs.test.ts b/tests/programs.test.ts new file mode 100644 index 0000000..bfb858d --- /dev/null +++ b/tests/programs.test.ts @@ -0,0 +1,70 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + getGetProgramUrl, + getListProgramsUrl, + getProgram, + listPrograms, +} from "../src/programs/programs"; +import { makeMockResponse } from "./helpers"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("URL builders", () => { + it("getListProgramsUrl", () => { + expect(getListProgramsUrl()).toBe("https://games.recf.org/api/v1/programs"); + }); + + it("getGetProgramUrl", () => { + expect(getGetProgramUrl("achieve")).toBe( + "https://games.recf.org/api/v1/programs/achieve", + ); + }); +}); + +describe("listPrograms", () => { + it("returns program list on success", async () => { + const mockData = { data: [{ slug: "achieve", name: "Achieve" }], meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await listPrograms(); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("calls fetch with GET method", async () => { + const mockFetch = vi.fn().mockResolvedValue(makeMockResponse(200, { data: [], meta: {} })); + vi.stubGlobal("fetch", mockFetch); + + await listPrograms(); + + expect(mockFetch).toHaveBeenCalledWith( + "https://games.recf.org/api/v1/programs", + expect.objectContaining({ method: "GET" }), + ); + }); +}); + +describe("getProgram", () => { + it("returns program data on success", async () => { + const mockData = { data: { slug: "achieve", name: "Achieve" }, meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await getProgram("achieve"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("returns error data on 404", async () => { + const mockError = { message: "Program not found" }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(404, mockError))); + + const result = await getProgram("unknown"); + + expect(result.status).toBe(404); + expect(result.data).toEqual(mockError); + }); +}); diff --git a/tests/q-a.test.ts b/tests/q-a.test.ts new file mode 100644 index 0000000..6541fa2 --- /dev/null +++ b/tests/q-a.test.ts @@ -0,0 +1,171 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + getGetQaRssUrl, + getGetQaUrl, + getListQaUrl, + getListRuleQaUrl, + getQa, + getQaRss, + listQa, + listRuleQa, +} from "../src/q-a/q-a"; +import { makeMockResponse } from "./helpers"; + +afterEach(() => { + vi.unstubAllGlobals(); +}); + +describe("URL builders", () => { + it("getListQaUrl without params", () => { + expect(getListQaUrl("achieve")).toBe( + "https://games.recf.org/api/v1/programs/achieve/qa", + ); + }); + + it("getListQaUrl with params", () => { + const url = getListQaUrl("achieve", { limit: 2 }); + expect(url).toBe("https://games.recf.org/api/v1/programs/achieve/qa?limit=2"); + }); + + it("getListQaUrl with empty params produces no query string", () => { + const url = getListQaUrl("achieve", {}); + expect(url).toBe("https://games.recf.org/api/v1/programs/achieve/qa"); + }); + + it("getGetQaUrl", () => { + expect(getGetQaUrl("achieve", "42")).toBe( + "https://games.recf.org/api/v1/programs/achieve/qa/42", + ); + }); + + it("getGetQaRssUrl", () => { + expect(getGetQaRssUrl("achieve")).toBe( + "https://games.recf.org/api/v1/programs/achieve/qa.rss", + ); + }); + + it("getListRuleQaUrl without params", () => { + expect(getListRuleQaUrl("achieve", "R1")).toBe( + "https://games.recf.org/api/v1/programs/achieve/rules/R1/qa", + ); + }); + + it("getListRuleQaUrl with params", () => { + const url = getListRuleQaUrl("achieve", "R1", { limit: 1 }); + expect(url).toBe( + "https://games.recf.org/api/v1/programs/achieve/rules/R1/qa?limit=1", + ); + }); +}); + +describe("listQa", () => { + it("returns Q&A list on success", async () => { + const mockData = { + data: { + questions: [{ qnaNumber: 1, subject: "Q?" }], + pagination: { total: 1, limit: 25, offset: 0 }, + }, + meta: {}, + }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await listQa("achieve"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("returns error data on 404", async () => { + const mockError = { message: "Not found" }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(404, mockError))); + + const result = await listQa("unknown"); + + expect(result.status).toBe(404); + expect(result.data).toEqual(mockError); + }); +}); + +describe("getQa", () => { + it("returns single Q&A entry on success", async () => { + const mockData = { data: { qnaNumber: 42, subject: "Is this legal?" }, meta: {} }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await getQa("achieve", "42"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); +}); + +describe("getQaRss", () => { + it("returns raw string when content-type is RSS", async () => { + const rssBody = ``; + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(makeMockResponse(200, rssBody, "application/rss+xml")), + ); + + const result = await getQaRss("achieve"); + + expect(result.status).toBe(200); + expect(result.data).toBe(rssBody); + }); + + it("returns parsed object when content-type is JSON", async () => { + const jsonBody = { items: [] }; + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(makeMockResponse(200, jsonBody, "application/json")), + ); + + const result = await getQaRss("achieve"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(jsonBody); + }); + + it("returns parsed error data on 404", async () => { + const mockError = { message: "Program not found" }; + vi.stubGlobal( + "fetch", + vi.fn().mockResolvedValue(makeMockResponse(404, mockError, "application/json")), + ); + + const result = await getQaRss("achieve"); + + expect(result.status).toBe(404); + expect(result.data).toEqual(mockError); + }); +}); + +describe("listRuleQa", () => { + it("returns rule Q&A list on success", async () => { + const mockData = { + data: { + ruleStableKey: "R1", + questions: [], + pagination: { total: 0, limit: 25, offset: 0 }, + }, + meta: {}, + }; + vi.stubGlobal("fetch", vi.fn().mockResolvedValue(makeMockResponse(200, mockData))); + + const result = await listRuleQa("achieve", "R1"); + + expect(result.status).toBe(200); + expect(result.data).toEqual(mockData); + }); + + it("calls fetch with correct URL including rule key", async () => { + const mockFetch = vi.fn().mockResolvedValue(makeMockResponse(200, {})); + vi.stubGlobal("fetch", mockFetch); + + await listRuleQa("achieve", "R1"); + + expect(mockFetch).toHaveBeenCalledWith( + "https://games.recf.org/api/v1/programs/achieve/rules/R1/qa", + expect.objectContaining({ method: "GET" }), + ); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 1b8a57d..8eed0d1 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -6,7 +6,6 @@ "target": "es2022", "declaration": true, "noUncheckedIndexedAccess": true, - "exactOptionalPropertyTypes": true, "strict": true, "isolatedModules": true, "noUncheckedSideEffectImports": true, diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..59f7390 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + }, +});