diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 7712bc0b..b83f4f0b 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -31,6 +31,9 @@ jobs: - name: Install dependencies run: pnpm install + - name: Install Playwright browsers + run: pnpm dlx playwright install chromium + - name: Build Rspack run: | pnpm build:rspack @@ -50,3 +53,6 @@ jobs: - name: Build Rsdoctor run: | pnpm build:rsdoctor + + - name: Test Rstest + run: pnpm test:rstest diff --git a/.gitignore b/.gitignore index 5a4c755c..65f56ca1 100644 --- a/.gitignore +++ b/.gitignore @@ -23,6 +23,7 @@ lib-cov # Coverage directory used by tools like istanbul coverage +!rstest/coverage/ *.lcov # nyc test coverage diff --git a/AGENTS.md b/AGENTS.md index 18efc26f..020a4b96 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -4,9 +4,33 @@ This file provides guidance for AI coding agents working in this repository. ## Repository Overview -- This is a monorepo of example projects for the Rstack ecosystem: Rspack, Rsbuild, Rspress, Rsdoctor, and Rslib. +- This is a monorepo of example projects for the Rstack ecosystem: Rspack, Rsbuild, Rspress, Rsdoctor, Rslib, and Rstest. - Package manager: `pnpm` (see `package.json#packageManager`). -- Workspace layout: `pnpm-workspace.yaml` includes `rspack/**`, `rsbuild/**`, `rslib/**`, `rspress/**`, `rsdoctor/**`. +- Workspace layout: `pnpm-workspace.yaml` includes `rspack/**`, `rsbuild/**`, `rslib/**`, `rspress/**`, `rsdoctor/**`, `rstest/**`. + +## Core Purpose of Examples + +**The primary goal of each example is to demonstrate "how a specific API achieves a specific effect through specific configuration".** + +When creating or modifying examples: +- **Keep it minimal**: Only include code necessary to demonstrate the target feature/API +- **Avoid over-engineering**: Don't add complex business logic that distracts from the core demonstration +- **Focus on the tool, not the ecosystem**: For example, in a test runner example, focus on the test runner's APIs (mocking, assertions, configuration), not on complex DOM manipulation or third-party library integrations +- **One concept per example**: Each example should ideally demonstrate one main feature or configuration pattern +- **Clarity over completeness**: A simple, clear example is better than a comprehensive but confusing one + +Example of good vs bad: +``` +# Good: Demonstrates rstest's mocking API +- Simple mock function usage +- Clear before/after assertions +- Minimal setup code + +# Bad: Demonstrates rstest's mocking API +- Complex React component with many interactions +- Detailed DOM testing with @testing-library +- Business logic mixed with test assertions +``` ## Quick Start (Local) diff --git a/biome.json b/biome.json index d3da8e8c..fd0d2cbe 100644 --- a/biome.json +++ b/biome.json @@ -13,11 +13,13 @@ "**/rspress/**", "**/rsdoctor/**", "**/rslib/**", + "**/rstest/**", "!**/dist", "!**/dist-*", "!**/doc_build", "!**/auto-imports.d.ts", - "!**/components.d.ts" + "!**/components.d.ts", + "!**/__snapshots__/**" ], "ignoreUnknown": true }, diff --git a/package.json b/package.json index 89f14efc..4c1d2b09 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,11 @@ "build:rsdoctor": "pnpm --filter \"rsdoctor-*\" build", "build:rspack": "pnpm --filter \"example-*\" build", "test:rspack": "pnpm --filter \"example-*\" test", + "test:rstest": "pnpm --filter \"rstest-*\" test", "build:rspress": "pnpm --filter \"rspress-*\" build", "build:rslib": "pnpm --filter \"rslib-*\" build", "prepare": "husky", - "sort-package-json": "npx sort-package-json \"rspack/*/package.json\" \"rsbuild/*/package.json\" \"rspress/*/package.json\" \"rsdoctor/*/package.json\" \"rslib/*/package.json\"" + "sort-package-json": "npx sort-package-json \"rspack/*/package.json\" \"rsbuild/*/package.json\" \"rspress/*/package.json\" \"rsdoctor/*/package.json\" \"rslib/*/package.json\" \"rstest/*/package.json\"" }, "lint-staged": { "*.{js,ts,cjs,mjs,d.cts,d.mts,jsx,tsx,json,jsonc}": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9d9c2de2..afdd6322 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -111,10 +111,10 @@ importers: devDependencies: '@module-federation/enhanced': specifier: ^0.22.0 - version: 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/rsbuild-plugin': specifier: ^0.22.0 - version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@rsbuild/core': specifier: ^1.7.1 version: 1.7.1 @@ -142,10 +142,10 @@ importers: devDependencies: '@module-federation/enhanced': specifier: ^0.22.0 - version: 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/rsbuild-plugin': specifier: ^0.22.0 - version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@rsbuild/core': specifier: ^1.7.1 version: 1.7.1 @@ -428,7 +428,7 @@ importers: version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) '@rstest/core': specifier: 0.7.8 - version: 0.7.8(jsdom@27.4.0(postcss@8.5.6)) + version: 0.7.8(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -871,7 +871,7 @@ importers: version: 1.7.1 '@rsbuild/plugin-vue2': specifier: ^1.0.5 - version: 1.0.5(@rsbuild/core@1.7.1)(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)) + version: 1.0.5(@rsbuild/core@1.7.1)(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1021,7 +1021,7 @@ importers: version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) '@rsdoctor/rspack-plugin': specifier: ^1.4.0 - version: 1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) + version: 1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) '@types/react': specifier: ^19.2.7 version: 19.2.7 @@ -1055,7 +1055,7 @@ importers: devDependencies: '@rsdoctor/rspack-plugin': specifier: ^1.4.0 - version: 1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) + version: 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) '@rspack/cli': specifier: 1.7.0 version: 1.7.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack-cli@6.0.1)(webpack@5.104.1) @@ -1095,7 +1095,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1134,7 +1134,7 @@ importers: devDependencies: '@module-federation/rsbuild-plugin': specifier: ^0.22.0 - version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@rsbuild/core': specifier: ~1.7.1 version: 1.7.1 @@ -1155,16 +1155,16 @@ importers: devDependencies: '@module-federation/enhanced': specifier: ^0.22.0 - version: 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/rsbuild-plugin': specifier: ^0.22.0 - version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/storybook-addon': specifier: ^5.0.2 - version: 5.0.2(@module-federation/sdk@0.22.0)(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack-virtual-modules@0.6.2)(webpack@5.104.1) + version: 5.0.2(@module-federation/sdk@0.22.0)(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack-virtual-modules@0.6.2)(webpack@5.104.1) '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1185,10 +1185,10 @@ importers: version: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) storybook-addon-rslib: specifier: ^3.2.0 - version: 3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) + version: 3.2.0(@rsbuild/core@1.7.2)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) storybook-react-rsbuild: specifier: ^3.2.0 - version: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1) + version: 3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1) rslib/module-federation/mf-remote: dependencies: @@ -1201,7 +1201,7 @@ importers: devDependencies: '@module-federation/rsbuild-plugin': specifier: ^0.22.0 - version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + version: 0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@rsbuild/core': specifier: ~1.7.1 version: 1.7.1 @@ -1237,7 +1237,7 @@ importers: version: 0.19.1(typescript@5.9.3) '@rstest/core': specifier: ^0.7.8 - version: 0.7.8(jsdom@27.4.0(postcss@8.5.6)) + version: 0.7.8(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) '@types/node': specifier: ^24.10.4 version: 24.10.4 @@ -1249,7 +1249,7 @@ importers: devDependencies: '@rsbuild/plugin-preact': specifier: ^1.7.0 - version: 1.7.0(@rsbuild/core@1.7.1)(preact@10.28.2) + version: 1.7.0(@rsbuild/core@1.7.2)(preact@10.28.2) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1268,7 +1268,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1290,7 +1290,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1312,10 +1312,10 @@ importers: devDependencies: '@rsbuild/plugin-less': specifier: ^1.5.0 - version: 1.5.0(@rsbuild/core@1.7.1) + version: 1.5.0(@rsbuild/core@1.7.2) '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1337,13 +1337,13 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) '@rstest/core': specifier: ^0.7.8 - version: 0.7.8(jsdom@27.4.0(postcss@8.5.6)) + version: 0.7.8(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -1371,10 +1371,10 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rsbuild/plugin-sass': specifier: ^1.4.0 - version: 1.4.0(@rsbuild/core@1.7.1) + version: 1.4.0(@rsbuild/core@1.7.2) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1422,10 +1422,10 @@ importers: version: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) storybook-addon-rslib: specifier: ^3.2.0 - version: 3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) + version: 3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) storybook-react-rsbuild: specifier: ^3.2.0 - version: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.2)) + version: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.2)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1438,7 +1438,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1466,7 +1466,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1487,10 +1487,10 @@ importers: devDependencies: '@rsbuild/plugin-babel': specifier: ^1.0.6 - version: 1.0.6(@rsbuild/core@1.7.1) + version: 1.0.6(@rsbuild/core@1.7.2) '@rsbuild/plugin-solid': specifier: ^1.0.6 - version: 1.0.6(@babel/core@7.28.5)(@rsbuild/core@1.7.1)(solid-js@1.9.10) + version: 1.0.6(@babel/core@7.28.5)(@rsbuild/core@1.7.2)(solid-js@1.9.10) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1508,10 +1508,10 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.4.2 - version: 1.4.2(@rsbuild/core@1.7.1)(webpack-hot-middleware@2.26.1) + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) '@rsbuild/plugin-sass': specifier: ^1.4.0 - version: 1.4.0(@rsbuild/core@1.7.1) + version: 1.4.0(@rsbuild/core@1.7.2) '@rslib/core': specifier: ^0.19.1 version: 0.19.1(typescript@5.9.3) @@ -1532,7 +1532,7 @@ importers: version: 0.19.1(typescript@5.9.3) rsbuild-plugin-unplugin-vue: specifier: ^0.1.0 - version: 0.1.0(@rsbuild/core@1.7.1)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) + version: 0.1.0(@rsbuild/core@1.7.2)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1550,7 +1550,7 @@ importers: version: 0.19.1(typescript@5.9.3) '@rstest/core': specifier: ^0.7.8 - version: 0.7.8(jsdom@27.4.0(postcss@8.5.6)) + version: 0.7.8(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) '@testing-library/jest-dom': specifier: ^6.9.1 version: 6.9.1 @@ -1565,7 +1565,7 @@ importers: version: 27.4.0(postcss@8.5.6) rsbuild-plugin-unplugin-vue: specifier: ^0.1.0 - version: 0.1.0(@rsbuild/core@1.7.1)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) + version: 0.1.0(@rsbuild/core@1.7.2)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -1601,10 +1601,10 @@ importers: version: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) storybook-addon-rslib: specifier: ^3.2.0 - version: 3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) + version: 3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3) storybook-vue3-rsbuild: specifier: ^3.2.0 - version: 3.2.0(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.3))(webpack@5.104.1(esbuild@0.27.2)) + version: 3.2.0(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.3))(webpack@5.104.1(esbuild@0.27.2)) typescript: specifier: ^5.9.3 version: 5.9.3 @@ -2251,7 +2251,7 @@ importers: version: 10.0.0(@babel/core@7.28.5)(webpack@5.104.1) html-webpack-plugin: specifier: ^5.6.5 - version: 5.6.5(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) + version: 5.6.5(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) react-refresh: specifier: ^0.18.0 version: 0.18.0 @@ -3693,6 +3693,141 @@ importers: specifier: ^19.2.3 version: 19.2.3(react@19.2.3) + rstest/browser-rsbuild-react: + devDependencies: + '@rsbuild/core': + specifier: ^1.4.2 + version: 1.7.2 + '@rsbuild/plugin-react': + specifier: ^1.4.2 + version: 1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1) + '@rstest/adapter-rsbuild': + specifier: ^0.1.0 + version: 0.1.0(@rsbuild/core@1.7.2) + '@rstest/browser': + specifier: ^0.7.9 + version: 0.7.9(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))(playwright@1.57.0) + '@rstest/browser-react': + specifier: ^0.7.9 + version: 0.7.9(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + '@testing-library/dom': + specifier: ^10.4.0 + version: 10.4.0 + '@testing-library/user-event': + specifier: ^14.6.1 + version: 14.6.1(@testing-library/dom@10.4.0) + '@types/react': + specifier: ^19.1.8 + version: 19.2.7 + '@types/react-dom': + specifier: ^19.1.6 + version: 19.2.3(@types/react@19.2.7) + playwright: + specifier: ^1.57.0 + version: 1.57.0 + react: + specifier: ^19.2.3 + version: 19.2.3 + react-dom: + specifier: ^19.2.3 + version: 19.2.3(react@19.2.3) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/browser-rsbuild-vanilla: + devDependencies: + '@rsbuild/core': + specifier: ^1.7.1 + version: 1.7.2 + '@rstest/browser': + specifier: ^0.7.9 + version: 0.7.9(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))(playwright@1.57.0) + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + playwright: + specifier: ^1.57.0 + version: 1.57.0 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/coverage: + devDependencies: + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + '@rstest/coverage-istanbul': + specifier: ^0.1.6 + version: 0.1.6(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6))) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/fake-timers: + devDependencies: + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/mocking: + devDependencies: + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/rsbuild-adapter: + devDependencies: + '@rsbuild/core': + specifier: ^1.7.2 + version: 1.7.2 + '@rstest/adapter-rsbuild': + specifier: ^0.1.0 + version: 0.1.0(@rsbuild/core@1.7.2) + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + happy-dom: + specifier: ^18.0.1 + version: 18.0.1 + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/rslib-adapter: + devDependencies: + '@rslib/core': + specifier: ^0.19.1 + version: 0.19.1(typescript@5.9.3) + '@rstest/adapter-rslib': + specifier: ^0.1.1 + version: 0.1.1(@rslib/core@0.19.1(typescript@5.9.3))(typescript@5.9.3) + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + + rstest/snapshot: + devDependencies: + '@rstest/core': + specifier: ^0.7.9 + version: 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + typescript: + specifier: ^5.8.3 + version: 5.9.3 + packages: '@aashutoshrathi/word-wrap@1.2.6': @@ -5712,12 +5847,12 @@ packages: '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} - '@jridgewell/trace-mapping@0.3.25': - resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} - '@jridgewell/trace-mapping@0.3.29': resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@jridgewell/trace-mapping@0.3.9': resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} @@ -6768,6 +6903,11 @@ packages: engines: {node: '>=18.12.0'} hasBin: true + '@rsbuild/core@1.7.2': + resolution: {integrity: sha512-VAFO6cM+cyg2ntxNW6g3tB2Jc5J5mpLjLluvm7VtW2uceNzyUlVv41o66Yp1t1ikxd3ljtqegViXem62JqzveA==} + engines: {node: '>=18.12.0'} + hasBin: true + '@rsbuild/plugin-babel@1.0.6': resolution: {integrity: sha512-tWnqG938MedKJx7U4F1lHb156VDtNzj7mSsi2ZoxZVBnECQE01/V6QTN1XKw7nWunGyGoETb+nQBGc+fkVZjvw==} peerDependencies: @@ -6947,6 +7087,11 @@ packages: cpu: [arm64] os: [darwin] + '@rspack/binding-darwin-arm64@1.7.1': + resolution: {integrity: sha512-3C0w0kfCHfgOH+AP/Dx1bm/b3AR/or5CmU22Abevek0m95ndU3iT902eLcm9JNiMQnDQLBQbolfj5P591t0oPg==} + cpu: [arm64] + os: [darwin] + '@rspack/binding-darwin-x64@1.3.12': resolution: {integrity: sha512-Sj4m+mCUxL7oCpdu7OmWT7fpBM7hywk5CM9RDc3D7StaBZbvNtNftafCrTZzTYKuZrKmemTh5SFzT5Tz7tf6GA==} cpu: [x64] @@ -6967,6 +7112,11 @@ packages: cpu: [x64] os: [darwin] + '@rspack/binding-darwin-x64@1.7.1': + resolution: {integrity: sha512-HTrBpdw2gWwcpJ3c8h4JF8B1YRNvrFT+K620ycttrlu/HvI4/U770BBJ/ej36R/hdh59JvMCGe+w49FyXv6rzg==} + cpu: [x64] + os: [darwin] + '@rspack/binding-linux-arm64-gnu@1.3.12': resolution: {integrity: sha512-7MuOxf3/Mhv4mgFdLTvgnt/J+VouNR65DEhorth+RZm3LEWojgoFEphSAMAvpvAOpYSS68Sw4SqsOZi719ia2w==} cpu: [arm64] @@ -6987,6 +7137,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-gnu@1.7.1': + resolution: {integrity: sha512-BX9yAPCO0WBFyOzKl9bSXT/cH27nnOJp02smIQMxfv7RNfwGkJg5GgakYcuYG+9U1HEFitBSzmwS2+dxDcAxlg==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-arm64-musl@1.3.12': resolution: {integrity: sha512-s6KKj20T9Z1bA8caIjU6EzJbwyDo1URNFgBAlafCT2UC6yX7flstDJJ38CxZacA9A2P24RuQK2/jPSZpWrTUFA==} cpu: [arm64] @@ -7007,6 +7162,11 @@ packages: cpu: [arm64] os: [linux] + '@rspack/binding-linux-arm64-musl@1.7.1': + resolution: {integrity: sha512-maBX19XyiVkxzh/NA79ALetCobc4zUyoWkWLeCGyW5xKzhPVFatJp+qCiHqHkqUZcgRo+1i5ihoZ2bXmelIeZg==} + cpu: [arm64] + os: [linux] + '@rspack/binding-linux-x64-gnu@1.3.12': resolution: {integrity: sha512-0w/sRREYbRgHgWvs2uMEJSLfvzbZkPHUg6CMcYQGNVK6axYRot6jPyKetyFYA9pR5fB5rsXegpnFaZaVrRIK2g==} cpu: [x64] @@ -7027,6 +7187,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-gnu@1.7.1': + resolution: {integrity: sha512-8KJAeBLiWcN7zEc9aaS7LRJPZVtZuQU8mCsn+fRhdQDSc+a9FcTN8b6Lw29z8cejwbU6Gxr/8wk5XGexMWFaZA==} + cpu: [x64] + os: [linux] + '@rspack/binding-linux-x64-musl@1.3.12': resolution: {integrity: sha512-jEdxkPymkRxbijDRsBGdhopcbGXiXDg59lXqIRkVklqbDmZ/O6DHm7gImmlx5q9FoWbz0gqJuOKBz4JqWxjWVA==} cpu: [x64] @@ -7047,6 +7212,11 @@ packages: cpu: [x64] os: [linux] + '@rspack/binding-linux-x64-musl@1.7.1': + resolution: {integrity: sha512-Gn9x5vhKRELvSoZ3ZjquY8eWtCXur0OsYnZ2/ump8mofM6IDaL7Qqu3Hf4Kud31PDH0tfz0jWf9piX32HHPmgg==} + cpu: [x64] + os: [linux] + '@rspack/binding-wasm32-wasi@1.6.7': resolution: {integrity: sha512-yx88EFdE9RP3hh7VhjjW6uc6wGU0KcpOcZp8T8E/a+X8L98fX0aVrtM1IDbndhmdluIMqGbfJNap2+QqOCY9Mw==} cpu: [wasm32] @@ -7059,6 +7229,10 @@ packages: resolution: {integrity: sha512-eaZzkGpxzVESmaX/UALMiQO+eNppe/i1VWQksGRfdoUu0rILqr/YDjsWFTcpbI9Dt3fg2kshHawBHxfwtxHcZQ==} cpu: [wasm32] + '@rspack/binding-wasm32-wasi@1.7.1': + resolution: {integrity: sha512-2r9M5iVchmsFkp3sz7A5YnMm2TfpkB71LK3AoaRWKMfvf5oFky0GSGISYd2TCBASO+X2Qskaq+B24Szo8zH5FA==} + cpu: [wasm32] + '@rspack/binding-win32-arm64-msvc@1.3.12': resolution: {integrity: sha512-ZRvUCb3TDLClAqcTsl/o9UdJf0B5CgzAxgdbnYJbldyuyMeTUB4jp20OfG55M3C2Nute2SNhu2bOOp9Se5Ongw==} cpu: [arm64] @@ -7079,6 +7253,11 @@ packages: cpu: [arm64] os: [win32] + '@rspack/binding-win32-arm64-msvc@1.7.1': + resolution: {integrity: sha512-/WIHp982yqqqAuiz2WLtf1ofo9d1lHDGZJ7flxFllb1iMgnUeSRyX6stxEi11K3Rg6pQa7FdCZGKX/engyj2bw==} + cpu: [arm64] + os: [win32] + '@rspack/binding-win32-ia32-msvc@1.3.12': resolution: {integrity: sha512-1TKPjuXStPJr14f3ZHuv40Xc/87jUXx10pzVtrPnw+f3hckECHrbYU/fvbVzZyuXbsXtkXpYca6ygCDRJAoNeQ==} cpu: [ia32] @@ -7099,6 +7278,11 @@ packages: cpu: [ia32] os: [win32] + '@rspack/binding-win32-ia32-msvc@1.7.1': + resolution: {integrity: sha512-Kpela29n+kDGGsss6q/3qTd6n9VW7TOQaiA7t1YLdCCl8qqcdKlz/vWjFMd2MqgcSGC/16PvChE4sgpUvryfCQ==} + cpu: [ia32] + os: [win32] + '@rspack/binding-win32-x64-msvc@1.3.12': resolution: {integrity: sha512-lCR0JfnYKpV+a6r2A2FdxyUKUS4tajePgpPJN5uXDgMGwrDtRqvx+d0BHhwjFudQVJq9VVbRaL89s2MQ6u+xYw==} cpu: [x64] @@ -7119,6 +7303,11 @@ packages: cpu: [x64] os: [win32] + '@rspack/binding-win32-x64-msvc@1.7.1': + resolution: {integrity: sha512-B/y4MWqP2Xeto1/HV0qtZNOMPSLrEVOqi2b7JSIXG/bhlf+3IAkDzEEoHs+ZikLR4C8hMaS0pVJsDGKFmGzC9A==} + cpu: [x64] + os: [win32] + '@rspack/binding@1.3.12': resolution: {integrity: sha512-4Ic8lV0+LCBfTlH5aIOujIRWZOtgmG223zC4L3o8WY/+ESAgpdnK6lSSMfcYgRanYLAy3HOmFIp20jwskMpbAg==} @@ -7131,6 +7320,9 @@ packages: '@rspack/binding@1.7.0': resolution: {integrity: sha512-xO+pZKG2dvU9CuRTTi+DcCc4p+CZhBJlvuYikBja/0a62cTntQV2PWV+/xU1a6Vbo89yNz158LR05nvjtKVwTw==} + '@rspack/binding@1.7.1': + resolution: {integrity: sha512-qVTV1/UWpMSZktvK5A8+HolgR1Qf0nYR3Gg4Vax5x3/BcHDpwGZ0fbdFRUirGVWH/XwxZ81zoI6F2SZq7xbX+w==} + '@rspack/cli@1.7.0': resolution: {integrity: sha512-4fGf1lvP9+JzpkVIljtZ+NrGn8JEsyVwNACDZPL4Heg1U2tStIde2RvzHLVs+0+NBVENu47v6tho5xMLcJPB7w==} hasBin: true @@ -7173,6 +7365,15 @@ packages: '@swc/helpers': optional: true + '@rspack/core@1.7.1': + resolution: {integrity: sha512-kRxfY8RRa6nU3/viDvAIP6CRpx+0rfXFRonPL0pHBx8u6HhV7m9rLEyaN6MWsLgNIAWkleFGb7tdo4ux2ljRJQ==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@swc/helpers': '>=0.5.1' + peerDependenciesMeta: + '@swc/helpers': + optional: true + '@rspack/dev-server@1.1.5': resolution: {integrity: sha512-cwz0qc6iqqoJhyWqxP7ZqE2wyYNHkBMQUXxoQ0tNoZ4YNRkDyQ4HVJ/3oPSmMKbvJk/iJ16u7xZmwG6sK47q/A==} engines: {node: '>= 18.12.0'} @@ -7334,6 +7535,38 @@ packages: resolution: {integrity: sha512-sEORXfNT85RffLSjo5+AOCpQBi77drvtW5bFSk9MOmzqpv9tSk8SkIsDyjzccoOveKXCu4DIoJHaVYhf5kJcYQ==} engines: {node: '>=14.17.6'} + '@rstest/adapter-rsbuild@0.1.0': + resolution: {integrity: sha512-DhH/EiSOkzQPRZk4sENTeeOZIBmKyZJDCjRQ+SPgtG/JWV9PS9gv4J6l2e5hV1c0PpMiniiOCxX3O4wRCDcF4Q==} + peerDependencies: + '@rsbuild/core': '*' + + '@rstest/adapter-rslib@0.1.1': + resolution: {integrity: sha512-R63E/DrtG4ol9wffAxgeq9ze6/Dm9wms0S6grm8maXAQ7qLooza/CTj4AH6aXeEjbpb64hfRGZy8VmZh9+fCfg==} + peerDependencies: + '@rslib/core': '>=0.18.6' + typescript: ^5 + peerDependenciesMeta: + typescript: + optional: true + + '@rstest/browser-react@0.7.9': + resolution: {integrity: sha512-UQ0R+GXon4vCR5eJ7+yN6HpOBAABkd8p1yFw0ZNZYcK/NMCfNwnLfz1h9+dD9Rf5PS/y0AtHA7xRbodTdDHvKA==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@rstest/core': workspace:^ + react: ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@rstest/browser@0.7.9': + resolution: {integrity: sha512-IFbo2YbT+ckQXVI7qu40ez/iraHPkEbHnHyP4D26XKqd9QHYne9jH4nU8N/ero2Oh4WqObv2rujfDUX/p5syRA==} + engines: {node: '>=18.12.0'} + peerDependencies: + '@rstest/core': workspace:^ + playwright: ^1.49.1 + peerDependenciesMeta: + playwright: + optional: true + '@rstest/core@0.7.8': resolution: {integrity: sha512-A7oN5GLMYuVkTGjXTH+69nz3wR0lxHUkWax+hVq+jNQmcf81ZqJosTLqN6jwFZdmCfHbj3iTd9ku6boo8tJlnA==} engines: {node: '>=18.12.0'} @@ -7347,6 +7580,24 @@ packages: jsdom: optional: true + '@rstest/core@0.7.9': + resolution: {integrity: sha512-RHNPS1MDUxtf+1Z0YZi+vIQ13SdvCbcbpzDA7XHcPziTRy2mAPg8nfcms+XzbIp95KXH75ucAhgAKNFO0QgARA==} + engines: {node: '>=18.12.0'} + hasBin: true + peerDependencies: + happy-dom: '*' + jsdom: '*' + peerDependenciesMeta: + happy-dom: + optional: true + jsdom: + optional: true + + '@rstest/coverage-istanbul@0.1.6': + resolution: {integrity: sha512-a7p3MCuKdbA89A0fL8IkXppvAEkl9+cUrqA5Nixgy/8DemJwJ+i9iDuQetLDaTdlXfNknIT5A6dQjKgoR4cWjw==} + peerDependencies: + '@rstest/core': ~0.7.9 + '@selderee/plugin-htmlparser2@0.11.0': resolution: {integrity: sha512-P33hHGdldxGabLFjPPpaTxVolMrzrcegejx+0GxjrIb9Zv48D8yAIA/QTDR2dFl7Uz7urX8aX6+5bCZslr+gWQ==} @@ -8159,6 +8410,9 @@ packages: '@types/node-forge@1.3.11': resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==} + '@types/node@20.19.27': + resolution: {integrity: sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug==} + '@types/node@24.10.0': resolution: {integrity: sha512-qzQZRBqkFsYyaSWXuEHc2WR9c0a0CXwiE5FWUvn7ZM+vdy1uZLfCunD38UzhuB7YN/J11ndbDBcTmOdxJo9Q7A==} @@ -8251,6 +8505,9 @@ packages: '@types/webpack@5.28.5': resolution: {integrity: sha512-wR87cgvxj3p6D0Crt1r5avwqffqPXUkNlnQ1mjU93G7gCuFjufZR4I6j8cz5g1F1tTYpfOOFvly+cmIQwL9wvw==} + '@types/whatwg-mimetype@3.0.2': + resolution: {integrity: sha512-c2AKvDT8ToxLIOUlN51gTiHXflsfIFisS4pO7pDPoKouJCESkhZnEy623gwP9laCy5lnLDAw1vAzu2vM2YLOrA==} + '@types/ws@8.5.10': resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} @@ -10444,6 +10701,10 @@ packages: resolution: {integrity: sha512-L8vK54CSjKB4pwlwx0YaqeBdUSGufaLHl/pEgD+EqnMrYCVUA8HzMjURALSyvOlC57e953yN7KyXS63qDoc3Rg==} engines: {node: '>=12.20'} + env-editor@1.3.0: + resolution: {integrity: sha512-EqiD/j01PooUbeWk+etUo2TWoocjoxMfGNYpS9e47glIJ5r8WepycIki+LCbonFbPdwlqY5ETeSTAJVMih4z4w==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + env-paths@2.2.1: resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} engines: {node: '>=6'} @@ -11004,6 +11265,11 @@ packages: fs.realpath@1.0.0: resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -11171,6 +11437,10 @@ packages: engines: {node: '>=0.4.7'} hasBin: true + happy-dom@18.0.1: + resolution: {integrity: sha512-qn+rKOW7KWpVTtgIUi6RVmTBZJSe2k0Db0vh1f7CWrWclkkc7/Q+FrOfkZIb2eiErLyqu5AXEzE7XthO9JVxRA==} + engines: {node: '>=20.0.0'} + has-ansi@2.0.0: resolution: {integrity: sha512-C8vBJ8DwUCx19vhm7urhTuUsr4/IyP6l4VzNQDv+ryHQObW3TTTp9yB68WpYgRe2bbaGuZ/se74IqFeVnMnLZg==} engines: {node: '>=0.10.0'} @@ -11896,6 +12166,10 @@ packages: resolution: {integrity: sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==} engines: {node: '>=8'} + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + iterare@1.2.1: resolution: {integrity: sha512-RKYVTCjAnRthyJes037NX/IiqeidgN1xc3j1RjFfECFp28A1GVwK9nA+i0rJPaHqSZwygLzRnFlzUuHFoWWy+Q==} engines: {node: '>=6'} @@ -12345,6 +12619,10 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + line-column-path@3.0.0: + resolution: {integrity: sha512-Atocnm7Wr9nuvAn97yEPQa3pcQI5eLQGBz+m6iTb+CVw+IOzYB9MrYK7jI7BfC9ISnT4Fu0eiwhAScV//rp4Hw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -13321,6 +13599,10 @@ packages: oniguruma-to-es@4.3.4: resolution: {integrity: sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA==} + open-editor@4.1.1: + resolution: {integrity: sha512-SYtGeZ9Zkzj/naoZaEF9LzwDYEGwuqQ4Fx5E3xdVRN98LFJjvMhG/ElByFEOVOiXepGra/Wi1fA4i/E1fXSBsw==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + open@10.2.0: resolution: {integrity: sha512-YgBpdJHPyQ2UE5x+hlSXcnejzAvD0b22U2OuAP+8OnlJT+PjWPxtgmGqKKc+RgTM63U9gN0YzrYc71R2WT/hTA==} engines: {node: '>=18'} @@ -13584,6 +13866,16 @@ packages: pkg-types@2.3.0: resolution: {integrity: sha512-SIqCzDRg0s9npO5XQ3tNZioRY1uK06lA41ynBC1YmFTmnY6FjUjVt6s4LoADmwoig1qqD0oK8h1p/8mlMx8Oig==} + playwright-core@1.57.0: + resolution: {integrity: sha512-agTcKlMw/mjBWOnD6kFZttAAGHgi/Nw0CZ2o6JqWSbMlI219lAFLZZCyqByTsvVAJq5XA5H8cA6PrvBRpBWEuQ==} + engines: {node: '>=18'} + hasBin: true + + playwright@1.57.0: + resolution: {integrity: sha512-ilYQj1s8sr2ppEJ2YVadYBN0Mb3mdo9J0wQ+UuDhzYqURwSoW4n1Xs5vs7ORwgDGmyEh33tRMeS8KhdkMoLXQw==} + engines: {node: '>=18'} + hasBin: true + pnpm-workspace-yaml@1.3.0: resolution: {integrity: sha512-Krb5q8Totd5mVuLx7we+EFHq/AfxA75nbfTm25Q1pIf606+RlaKUG+PXH8SDihfe5b5k4H09gE+sL47L1t5lbw==} @@ -15284,6 +15576,9 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + swc-plugin-coverage-instrument@0.0.32: + resolution: {integrity: sha512-KsO1xNQdcVb331Rm98Op6uYf36/CYdF1kILQxOddmtCNlRLISpCUqHcnogu+QgELZvG48oie+w0fzS8uktjcEA==} + swc-plugin-css-modules@8.0.0: resolution: {integrity: sha512-h7YgyHwCYlcOLhrt3lKOs8ulYAqDtxc5bVStsjHDw+aUmyfPK7vFejVvfEeYvF2QQ7qXoFoJTUfcgOKBdiHVRg==} @@ -15703,6 +15998,9 @@ packages: unconfig@7.4.2: resolution: {integrity: sha512-nrMlWRQ1xdTjSnSUqvYqJzbTBFugoqHobQj58B2bc8qxHKBBHMNNsWQFP3Cd3/JZK907voM2geYPWqD4VK3MPQ==} + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.16.0: resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} @@ -16337,6 +16635,10 @@ packages: engines: {node: '>=18'} deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation + whatwg-mimetype@3.0.0: + resolution: {integrity: sha512-nt+N2dzIutVRxARx1nghPKGv1xHikU7HKdfafKkLNLindmPU/ch3U31NOCGGA/dmPcmb1VlofO0vnKAcsm0o/Q==} + engines: {node: '>=12'} + whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} @@ -16642,7 +16944,7 @@ snapshots: '@ampproject/remapping@2.3.0': dependencies: '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@antfu/ni@27.0.1': dependencies: @@ -16803,7 +17105,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.4 '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.25 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/generator@7.26.5': @@ -16811,7 +17113,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/generator@7.28.0': @@ -16819,7 +17121,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/generator@7.28.3': @@ -16827,7 +17129,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/generator@7.28.5': @@ -16835,7 +17137,7 @@ snapshots: '@babel/parser': 7.28.5 '@babel/types': 7.28.5 '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 jsesc: 3.0.2 '@babel/helper-annotate-as-pure@7.27.1': @@ -19287,7 +19589,7 @@ snapshots: '@jest/test-result': 30.2.0 '@jest/transform': 30.2.0 '@jest/types': 30.2.0 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@types/node': 24.10.4 chalk: 4.1.2 collect-v8-coverage: 1.0.2 @@ -19325,7 +19627,7 @@ snapshots: '@jest/source-map@30.0.1': dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 callsites: 3.1.0 graceful-fs: 4.2.11 @@ -19347,7 +19649,7 @@ snapshots: dependencies: '@babel/core': 7.28.5 '@jest/types': 30.2.0 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 babel-plugin-istanbul: 7.0.1 chalk: 4.1.2 convert-source-map: 2.0.0 @@ -19395,13 +19697,13 @@ snapshots: '@jridgewell/gen-mapping@0.3.12': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/gen-mapping@0.3.5': dependencies: '@jridgewell/set-array': 1.2.1 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/remapping@2.3.5': dependencies: @@ -19415,16 +19717,16 @@ snapshots: '@jridgewell/source-map@0.3.5': dependencies: '@jridgewell/gen-mapping': 0.3.12 - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@jridgewell/sourcemap-codec@1.5.5': {} - '@jridgewell/trace-mapping@0.3.25': + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 - '@jridgewell/trace-mapping@0.3.29': + '@jridgewell/trace-mapping@0.3.31': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 @@ -19675,7 +19977,7 @@ snapshots: - supports-color - utf-8-validate - '@module-federation/enhanced@0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': + '@module-federation/enhanced@0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': dependencies: '@module-federation/bridge-react-webpack-plugin': 0.22.0 '@module-federation/cli': 0.22.0(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) @@ -19685,7 +19987,7 @@ snapshots: '@module-federation/inject-external-runtime-core-plugin': 0.22.0(@module-federation/runtime-tools@0.22.0) '@module-federation/managers': 0.22.0 '@module-federation/manifest': 0.22.0(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) - '@module-federation/rspack': 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) + '@module-federation/rspack': 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) '@module-federation/runtime-tools': 0.22.0 '@module-federation/sdk': 0.22.0 btoa: 1.2.1 @@ -19735,9 +20037,9 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/node@2.7.26(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': + '@module-federation/node@2.7.26(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': dependencies: - '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/runtime': 0.22.0 '@module-federation/sdk': 0.22.0 btoa: 1.2.1 @@ -19756,10 +20058,10 @@ snapshots: - utf-8-validate - vue-tsc - '@module-federation/rsbuild-plugin@0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': + '@module-federation/rsbuild-plugin@0.22.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': dependencies: - '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) - '@module-federation/node': 2.7.26(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/node': 2.7.26(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) '@module-federation/sdk': 0.22.0 fs-extra: 11.3.0 optionalDependencies: @@ -19777,7 +20079,28 @@ snapshots: - vue-tsc - webpack - '@module-federation/rspack@0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))': + '@module-federation/rsbuild-plugin@0.22.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1)': + dependencies: + '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/node': 2.7.26(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/sdk': 0.22.0 + fs-extra: 11.3.0 + optionalDependencies: + '@rsbuild/core': 1.7.2 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - debug + - next + - react + - react-dom + - supports-color + - typescript + - utf-8-validate + - vue-tsc + - webpack + + '@module-federation/rspack@0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))': dependencies: '@module-federation/bridge-react-webpack-plugin': 0.22.0 '@module-federation/dts-plugin': 0.22.0(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) @@ -19786,7 +20109,7 @@ snapshots: '@module-federation/manifest': 0.22.0(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3)) '@module-federation/runtime-tools': 0.22.0 '@module-federation/sdk': 0.22.0 - '@rspack/core': 1.7.0(@swc/helpers@0.5.18) + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) btoa: 1.2.1 optionalDependencies: typescript: 5.9.3 @@ -19851,12 +20174,12 @@ snapshots: '@module-federation/sdk@0.22.0': {} - '@module-federation/storybook-addon@5.0.2(@module-federation/sdk@0.22.0)(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack-virtual-modules@0.6.2)(webpack@5.104.1)': + '@module-federation/storybook-addon@5.0.2(@module-federation/sdk@0.22.0)(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack-virtual-modules@0.6.2)(webpack@5.104.1)': dependencies: - '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) + '@module-federation/enhanced': 0.22.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(typescript@5.9.3)(vue-tsc@3.2.1(typescript@5.9.3))(webpack@5.104.1) optionalDependencies: '@module-federation/sdk': 0.22.0 - '@rsbuild/core': 1.7.1 + '@rsbuild/core': 1.7.2 webpack: 5.104.1(webpack-cli@6.0.1) webpack-virtual-modules: 0.6.2 transitivePeerDependencies: @@ -20528,6 +20851,14 @@ snapshots: core-js: 3.47.0 jiti: 2.6.1 + '@rsbuild/core@1.7.2': + dependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + '@rspack/lite-tapable': 1.1.0 + '@swc/helpers': 0.5.18 + core-js: 3.47.0 + jiti: 2.6.1 + '@rsbuild/plugin-babel@1.0.6(@rsbuild/core@1.6.15)': dependencies: '@babel/core': 7.28.0 @@ -20556,6 +20887,20 @@ snapshots: transitivePeerDependencies: - supports-color + '@rsbuild/plugin-babel@1.0.6(@rsbuild/core@1.7.2)': + dependencies: + '@babel/core': 7.28.0 + '@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.28.0) + '@babel/plugin-transform-class-properties': 7.27.1(@babel/core@7.28.0) + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@rsbuild/core': 1.7.2 + '@types/babel__core': 7.20.5 + deepmerge: 4.3.1 + reduce-configs: 1.1.0 + upath: 2.0.1 + transitivePeerDependencies: + - supports-color + '@rsbuild/plugin-check-syntax@1.5.0(@rsbuild/core@1.7.1)': dependencies: acorn: 8.15.0 @@ -20585,6 +20930,12 @@ snapshots: deepmerge: 4.3.1 reduce-configs: 1.1.1 + '@rsbuild/plugin-less@1.5.0(@rsbuild/core@1.7.2)': + dependencies: + '@rsbuild/core': 1.7.2 + deepmerge: 4.3.1 + reduce-configs: 1.1.1 + '@rsbuild/plugin-mdx@1.1.0(@rsbuild/core@1.7.1)(webpack@5.104.1)': dependencies: '@mdx-js/loader': 3.1.1(webpack@5.104.1) @@ -20604,6 +20955,16 @@ snapshots: transitivePeerDependencies: - preact + '@rsbuild/plugin-preact@1.7.0(@rsbuild/core@1.7.2)(preact@10.28.2)': + dependencies: + '@prefresh/core': 1.5.9(preact@10.28.2) + '@prefresh/utils': 1.2.1 + '@rsbuild/core': 1.7.2 + '@rspack/plugin-preact-refresh': 1.1.4(@prefresh/core@1.5.9(preact@10.28.2))(@prefresh/utils@1.2.1) + '@swc/plugin-prefresh': 12.3.0 + transitivePeerDependencies: + - preact + '@rsbuild/plugin-react@1.3.5(@rsbuild/core@1.3.22)(webpack-hot-middleware@2.26.1)': dependencies: '@rsbuild/core': 1.3.22 @@ -20628,6 +20989,14 @@ snapshots: transitivePeerDependencies: - webpack-hot-middleware + '@rsbuild/plugin-react@1.4.2(@rsbuild/core@1.7.2)(webpack-hot-middleware@2.26.1)': + dependencies: + '@rsbuild/core': 1.7.2 + '@rspack/plugin-react-refresh': 1.5.2(react-refresh@0.18.0)(webpack-hot-middleware@2.26.1) + react-refresh: 0.18.0 + transitivePeerDependencies: + - webpack-hot-middleware + '@rsbuild/plugin-sass@1.3.1(@rsbuild/core@1.3.22)': dependencies: '@rsbuild/core': 1.3.22 @@ -20637,9 +21006,9 @@ snapshots: reduce-configs: 1.1.1 sass-embedded: 1.93.2 - '@rsbuild/plugin-sass@1.4.0(@rsbuild/core@1.7.1)': + '@rsbuild/plugin-sass@1.4.0(@rsbuild/core@1.7.2)': dependencies: - '@rsbuild/core': 1.7.1 + '@rsbuild/core': 1.7.2 deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.6 @@ -20657,6 +21026,17 @@ snapshots: - solid-js - supports-color + '@rsbuild/plugin-solid@1.0.6(@babel/core@7.28.5)(@rsbuild/core@1.7.2)(solid-js@1.9.10)': + dependencies: + '@rsbuild/core': 1.7.2 + '@rsbuild/plugin-babel': 1.0.6(@rsbuild/core@1.7.2) + babel-preset-solid: 1.9.8(@babel/core@7.28.5)(solid-js@1.9.10) + solid-refresh: 0.6.3(solid-js@1.9.10) + transitivePeerDependencies: + - '@babel/core' + - solid-js + - supports-color + '@rsbuild/plugin-styled-components@1.6.0(@rsbuild/core@1.7.1)': dependencies: '@swc/plugin-styled-components': 12.0.1 @@ -20682,25 +21062,37 @@ snapshots: - svelte - typescript - '@rsbuild/plugin-type-check@1.3.2(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(typescript@5.9.3)': + '@rsbuild/plugin-type-check@1.3.2(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3)': dependencies: deepmerge: 4.3.1 json5: 2.2.3 reduce-configs: 1.1.1 - ts-checker-rspack-plugin: 1.2.3(@rspack/core@1.7.0(@swc/helpers@0.5.18))(typescript@5.9.3) + ts-checker-rspack-plugin: 1.2.3(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3) optionalDependencies: '@rsbuild/core': 1.7.1 transitivePeerDependencies: - '@rspack/core' - typescript + '@rsbuild/plugin-type-check@1.3.2(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3)': + dependencies: + deepmerge: 4.3.1 + json5: 2.2.3 + reduce-configs: 1.1.1 + ts-checker-rspack-plugin: 1.2.3(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3) + optionalDependencies: + '@rsbuild/core': 1.7.2 + transitivePeerDependencies: + - '@rspack/core' + - typescript + '@rsbuild/plugin-umd@1.0.5(@rsbuild/core@1.7.1)': optionalDependencies: '@rsbuild/core': 1.7.1 - '@rsbuild/plugin-vue2@1.0.5(@rsbuild/core@1.7.1)(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1))': + '@rsbuild/plugin-vue2@1.0.5(@rsbuild/core@1.7.1)(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1))': dependencies: - vue-loader: 15.11.1(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1))(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(pug@3.0.2)(webpack@5.104.1) + vue-loader: 15.11.1(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1))(webpack@5.104.1) webpack: 5.104.1(webpack-cli@6.0.1) optionalDependencies: '@rsbuild/core': 1.7.1 @@ -20783,7 +21175,29 @@ snapshots: '@rsdoctor/client@1.4.0': {} - '@rsdoctor/core@1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': + '@rsdoctor/core@1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@rsbuild/plugin-check-syntax': 1.5.0(@rsbuild/core@1.7.1) + '@rsdoctor/graph': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/sdk': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/types': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/utils': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + browserslist-load-config: 1.0.1 + enhanced-resolve: 5.12.0 + es-toolkit: 1.41.0 + filesize: 10.1.6 + fs-extra: 11.3.2 + semver: 7.7.3 + source-map: 0.7.6 + transitivePeerDependencies: + - '@rsbuild/core' + - '@rspack/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + + '@rsdoctor/core@1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': dependencies: '@rsbuild/plugin-check-syntax': 1.5.0(@rsbuild/core@1.7.1) '@rsdoctor/graph': 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) @@ -20816,9 +21230,36 @@ snapshots: - '@rspack/core' - webpack - '@rsdoctor/rspack-plugin@1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': + '@rsdoctor/graph@1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@rsdoctor/types': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/utils': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + es-toolkit: 1.41.0 + path-browserify: 1.0.1 + source-map: 0.7.6 + transitivePeerDependencies: + - '@rspack/core' + - webpack + + '@rsdoctor/rspack-plugin@1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@rsdoctor/core': 1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/graph': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/sdk': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/types': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/utils': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + optionalDependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + transitivePeerDependencies: + - '@rsbuild/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + + '@rsdoctor/rspack-plugin@1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': dependencies: - '@rsdoctor/core': 1.4.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/core': 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) '@rsdoctor/graph': 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) '@rsdoctor/sdk': 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) '@rsdoctor/types': 1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1) @@ -20848,6 +21289,22 @@ snapshots: - utf-8-validate - webpack + '@rsdoctor/sdk@1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@rsdoctor/client': 1.4.0 + '@rsdoctor/graph': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/types': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@rsdoctor/utils': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + safer-buffer: 2.1.2 + socket.io: 4.8.1 + tapable: 2.2.3 + transitivePeerDependencies: + - '@rspack/core' + - bufferutil + - supports-color + - utf-8-validate + - webpack + '@rsdoctor/types@1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': dependencies: '@types/connect': 3.4.38 @@ -20858,6 +21315,16 @@ snapshots: '@rspack/core': 1.7.0(@swc/helpers@0.5.18) webpack: 5.104.1(webpack-cli@6.0.1) + '@rsdoctor/types@1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@types/connect': 3.4.38 + '@types/estree': 1.0.5 + '@types/tapable': 2.2.7 + source-map: 0.7.6 + optionalDependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + webpack: 5.104.1(webpack-cli@6.0.1) + '@rsdoctor/utils@1.4.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(webpack@5.104.1)': dependencies: '@babel/code-frame': 7.26.2 @@ -20879,6 +21346,27 @@ snapshots: - '@rspack/core' - webpack + '@rsdoctor/utils@1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1)': + dependencies: + '@babel/code-frame': 7.26.2 + '@rsdoctor/types': 1.4.0(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + '@types/estree': 1.0.5 + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + acorn-walk: 8.3.4 + deep-eql: 4.1.4 + envinfo: 7.21.0 + fs-extra: 11.3.2 + get-port: 5.1.1 + json-stream-stringify: 3.0.1 + lines-and-columns: 2.0.4 + picocolors: 1.1.1 + rslog: 1.2.11 + strip-ansi: 6.0.1 + transitivePeerDependencies: + - '@rspack/core' + - webpack + '@rslib/core@0.19.1(typescript@5.9.3)': dependencies: '@rsbuild/core': 1.7.1 @@ -20900,6 +21388,9 @@ snapshots: '@rspack/binding-darwin-arm64@1.7.0': optional: true + '@rspack/binding-darwin-arm64@1.7.1': + optional: true + '@rspack/binding-darwin-x64@1.3.12': optional: true @@ -20912,6 +21403,9 @@ snapshots: '@rspack/binding-darwin-x64@1.7.0': optional: true + '@rspack/binding-darwin-x64@1.7.1': + optional: true + '@rspack/binding-linux-arm64-gnu@1.3.12': optional: true @@ -20924,6 +21418,9 @@ snapshots: '@rspack/binding-linux-arm64-gnu@1.7.0': optional: true + '@rspack/binding-linux-arm64-gnu@1.7.1': + optional: true + '@rspack/binding-linux-arm64-musl@1.3.12': optional: true @@ -20936,6 +21433,9 @@ snapshots: '@rspack/binding-linux-arm64-musl@1.7.0': optional: true + '@rspack/binding-linux-arm64-musl@1.7.1': + optional: true + '@rspack/binding-linux-x64-gnu@1.3.12': optional: true @@ -20948,6 +21448,9 @@ snapshots: '@rspack/binding-linux-x64-gnu@1.7.0': optional: true + '@rspack/binding-linux-x64-gnu@1.7.1': + optional: true + '@rspack/binding-linux-x64-musl@1.3.12': optional: true @@ -20960,6 +21463,9 @@ snapshots: '@rspack/binding-linux-x64-musl@1.7.0': optional: true + '@rspack/binding-linux-x64-musl@1.7.1': + optional: true + '@rspack/binding-wasm32-wasi@1.6.7': dependencies: '@napi-rs/wasm-runtime': 1.0.7 @@ -20975,6 +21481,11 @@ snapshots: '@napi-rs/wasm-runtime': 1.0.7 optional: true + '@rspack/binding-wasm32-wasi@1.7.1': + dependencies: + '@napi-rs/wasm-runtime': 1.0.7 + optional: true + '@rspack/binding-win32-arm64-msvc@1.3.12': optional: true @@ -20987,6 +21498,9 @@ snapshots: '@rspack/binding-win32-arm64-msvc@1.7.0': optional: true + '@rspack/binding-win32-arm64-msvc@1.7.1': + optional: true + '@rspack/binding-win32-ia32-msvc@1.3.12': optional: true @@ -20999,6 +21513,9 @@ snapshots: '@rspack/binding-win32-ia32-msvc@1.7.0': optional: true + '@rspack/binding-win32-ia32-msvc@1.7.1': + optional: true + '@rspack/binding-win32-x64-msvc@1.3.12': optional: true @@ -21011,6 +21528,9 @@ snapshots: '@rspack/binding-win32-x64-msvc@1.7.0': optional: true + '@rspack/binding-win32-x64-msvc@1.7.1': + optional: true + '@rspack/binding@1.3.12': optionalDependencies: '@rspack/binding-darwin-arm64': 1.3.12 @@ -21062,6 +21582,19 @@ snapshots: '@rspack/binding-win32-ia32-msvc': 1.7.0 '@rspack/binding-win32-x64-msvc': 1.7.0 + '@rspack/binding@1.7.1': + optionalDependencies: + '@rspack/binding-darwin-arm64': 1.7.1 + '@rspack/binding-darwin-x64': 1.7.1 + '@rspack/binding-linux-arm64-gnu': 1.7.1 + '@rspack/binding-linux-arm64-musl': 1.7.1 + '@rspack/binding-linux-x64-gnu': 1.7.1 + '@rspack/binding-linux-x64-musl': 1.7.1 + '@rspack/binding-wasm32-wasi': 1.7.1 + '@rspack/binding-win32-arm64-msvc': 1.7.1 + '@rspack/binding-win32-ia32-msvc': 1.7.1 + '@rspack/binding-win32-x64-msvc': 1.7.1 + '@rspack/cli@1.7.0(@rspack/core@1.7.0(@swc/helpers@0.5.18))(@types/express@5.0.6)': dependencies: '@discoveryjs/json-ext': 0.5.7 @@ -21127,6 +21660,14 @@ snapshots: optionalDependencies: '@swc/helpers': 0.5.18 + '@rspack/core@1.7.1(@swc/helpers@0.5.18)': + dependencies: + '@module-federation/runtime-tools': 0.22.0 + '@rspack/binding': 1.7.1 + '@rspack/lite-tapable': 1.1.0 + optionalDependencies: + '@swc/helpers': 0.5.18 + '@rspack/dev-server@1.1.5(@rspack/core@1.7.0(@swc/helpers@0.5.18))(@types/express@4.17.21)(webpack@5.104.1)': dependencies: '@rspack/core': 1.7.0(@swc/helpers@0.5.18) @@ -21437,14 +21978,66 @@ snapshots: react-helmet-async: 1.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) react-syntax-highlighter: 15.6.1(react@18.3.1) - '@rstest/core@0.7.8(jsdom@27.4.0(postcss@8.5.6))': + '@rstest/adapter-rsbuild@0.1.0(@rsbuild/core@1.7.2)': + dependencies: + '@rsbuild/core': 1.7.2 + + '@rstest/adapter-rslib@0.1.1(@rslib/core@0.19.1(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@rslib/core': 0.19.1(typescript@5.9.3) + optionalDependencies: + typescript: 5.9.3 + + '@rstest/browser-react@0.7.9(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)': + dependencies: + '@rstest/core': 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + + '@rstest/browser@0.7.9(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))(playwright@1.57.0)': + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + '@rstest/core': 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + convert-source-map: 2.0.0 + open-editor: 4.1.1 + pathe: 2.0.3 + sirv: 2.0.4 + ws: 8.18.3 + optionalDependencies: + playwright: 1.57.0 + transitivePeerDependencies: + - bufferutil + - utf-8-validate + + '@rstest/core@0.7.8(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6))': + dependencies: + '@rsbuild/core': 1.7.1 + '@types/chai': 5.2.3 + tinypool: 1.1.1 + optionalDependencies: + happy-dom: 18.0.1 + jsdom: 27.4.0(postcss@8.5.6) + + '@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6))': dependencies: '@rsbuild/core': 1.7.1 '@types/chai': 5.2.3 tinypool: 1.1.1 optionalDependencies: + happy-dom: 18.0.1 jsdom: 27.4.0(postcss@8.5.6) + '@rstest/coverage-istanbul@0.1.6(@rstest/core@0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)))': + dependencies: + '@rstest/core': 0.7.9(happy-dom@18.0.1)(jsdom@27.4.0(postcss@8.5.6)) + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + swc-plugin-coverage-instrument: 0.0.32 + transitivePeerDependencies: + - supports-color + '@selderee/plugin-htmlparser2@0.11.0': dependencies: domhandler: 5.0.3 @@ -22352,6 +22945,10 @@ snapshots: dependencies: '@types/node': 24.10.4 + '@types/node@20.19.27': + dependencies: + undici-types: 6.21.0 + '@types/node@24.10.0': dependencies: undici-types: 7.16.0 @@ -22443,7 +23040,7 @@ snapshots: '@types/webpack@5.28.5(webpack-cli@6.0.1)': dependencies: - '@types/node': 25.0.3 + '@types/node': 24.10.4 tapable: 2.3.0 webpack: 5.104.1(webpack-cli@6.0.1) transitivePeerDependencies: @@ -22453,6 +23050,8 @@ snapshots: - webpack-cli optional: true + '@types/whatwg-mimetype@3.0.2': {} + '@types/ws@8.5.10': dependencies: '@types/node': 24.10.4 @@ -24392,6 +24991,20 @@ snapshots: '@rspack/core': 1.7.0(@swc/helpers@0.5.18) webpack: 5.104.1(webpack-cli@6.0.1) + css-loader@7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1): + dependencies: + icss-utils: 5.1.0(postcss@8.4.49) + postcss: 8.4.49 + postcss-modules-extract-imports: 3.1.0(postcss@8.4.49) + postcss-modules-local-by-default: 4.0.5(postcss@8.4.49) + postcss-modules-scope: 3.2.0(postcss@8.4.49) + postcss-modules-values: 4.0.0(postcss@8.4.49) + postcss-value-parser: 4.2.0 + semver: 7.6.3 + optionalDependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + webpack: 5.104.1(webpack-cli@6.0.1) + css-mediaquery@0.1.2: {} css-select@4.3.0: @@ -24927,6 +25540,8 @@ snapshots: fromentries: 1.3.2 java-properties: 1.0.2 + env-editor@1.3.0: {} + env-paths@2.2.1: {} envinfo@7.14.0: {} @@ -25785,6 +26400,9 @@ snapshots: fs.realpath@1.0.0: {} + fsevents@2.3.2: + optional: true + fsevents@2.3.3: optional: true @@ -25975,6 +26593,12 @@ snapshots: uglify-js: 3.19.3 optional: true + happy-dom@18.0.1: + dependencies: + '@types/node': 20.19.27 + '@types/whatwg-mimetype': 3.0.2 + whatwg-mimetype: 3.0.0 + has-ansi@2.0.0: dependencies: ansi-regex: 2.1.1 @@ -26363,6 +26987,17 @@ snapshots: '@rspack/core': 1.7.0(@swc/helpers@0.5.18) webpack: 5.104.1(webpack-cli@6.0.1) + html-webpack-plugin@5.6.5(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1): + dependencies: + '@types/html-minifier-terser': 6.1.0 + html-minifier-terser: 6.1.0 + lodash: 4.17.21 + pretty-error: 4.0.0 + tapable: 2.3.0 + optionalDependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + webpack: 5.104.1(webpack-cli@6.0.1) + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -26848,7 +27483,7 @@ snapshots: istanbul-lib-source-maps@5.0.6: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 debug: 4.4.3 istanbul-lib-coverage: 3.2.2 transitivePeerDependencies: @@ -26859,6 +27494,11 @@ snapshots: html-escaper: 2.0.2 istanbul-lib-report: 3.0.1 + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + iterare@1.2.1: {} jackspeak@3.4.3: @@ -27532,6 +28172,10 @@ snapshots: lilconfig@3.1.3: {} + line-column-path@3.0.0: + dependencies: + type-fest: 2.19.0 + lines-and-columns@1.2.4: {} lines-and-columns@2.0.4: {} @@ -29031,6 +29675,13 @@ snapshots: regex: 6.0.1 regex-recursion: 6.0.2 + open-editor@4.1.1: + dependencies: + env-editor: 1.3.0 + execa: 5.1.1 + line-column-path: 3.0.0 + open: 8.4.2 + open@10.2.0: dependencies: default-browser: 5.4.0 @@ -29314,6 +29965,14 @@ snapshots: exsolve: 1.0.7 pathe: 2.0.3 + playwright-core@1.57.0: {} + + playwright@1.57.0: + dependencies: + playwright-core: 1.57.0 + optionalDependencies: + fsevents: 2.3.2 + pnpm-workspace-yaml@1.3.0: dependencies: yaml: 2.8.1 @@ -30250,6 +30909,13 @@ snapshots: optionalDependencies: '@rsbuild/core': 1.7.1 + rsbuild-plugin-html-minifier-terser@1.1.2(@rsbuild/core@1.7.2): + dependencies: + '@types/html-minifier-terser': 7.0.2 + html-minifier-terser: 7.2.0 + optionalDependencies: + '@rsbuild/core': 1.7.2 + rsbuild-plugin-unplugin-vue@0.1.0(@rsbuild/core@1.7.1)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1): dependencies: unplugin-vue: 6.2.0(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) @@ -30270,6 +30936,26 @@ snapshots: - vue - yaml + rsbuild-plugin-unplugin-vue@0.1.0(@rsbuild/core@1.7.2)(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1): + dependencies: + unplugin-vue: 6.2.0(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(vue@3.5.21(typescript@5.9.3))(yaml@2.8.1) + optionalDependencies: + '@rsbuild/core': 1.7.2 + transitivePeerDependencies: + - '@types/node' + - jiti + - less + - lightningcss + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - vue + - yaml + rslog@1.2.11: {} rspack-manifest-plugin@5.2.0(@rspack/core@1.7.0(@swc/helpers@0.5.18)): @@ -30981,18 +31667,26 @@ snapshots: es-errors: 1.3.0 internal-slot: 1.1.0 - storybook-addon-rslib@3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3): + storybook-addon-rslib@3.2.0(@rsbuild/core@1.7.1)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3): dependencies: '@rsbuild/core': 1.7.1 '@rslib/core': 0.19.1(typescript@5.9.3) - storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + optionalDependencies: + typescript: 5.9.3 + + storybook-addon-rslib@3.2.0(@rsbuild/core@1.7.2)(@rslib/core@0.19.1(typescript@5.9.3))(storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)))(typescript@5.9.3): + dependencies: + '@rsbuild/core': 1.7.2 + '@rslib/core': 0.19.1(typescript@5.9.3) + storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) optionalDependencies: typescript: 5.9.3 - storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)): + storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)): dependencies: '@rsbuild/core': 1.7.1 - '@rsbuild/plugin-type-check': 1.3.2(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(typescript@5.9.3) + '@rsbuild/plugin-type-check': 1.3.2(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3) '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) browser-assert: 1.2.1 case-sensitive-paths-webpack-plugin: 2.4.0 @@ -31020,7 +31714,38 @@ snapshots: - msw - vite - storybook-react-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.2)): + storybook-builder-rsbuild@3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)): + dependencies: + '@rsbuild/core': 1.7.2 + '@rsbuild/plugin-type-check': 1.3.2(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3) + '@vitest/mocker': 3.2.4(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + browser-assert: 1.2.1 + case-sensitive-paths-webpack-plugin: 2.4.0 + cjs-module-lexer: 2.1.1 + constants-browserify: 1.0.0 + es-module-lexer: 1.7.0 + fs-extra: 11.3.2 + magic-string: 0.30.21 + path-browserify: 1.0.1 + picocolors: 1.1.1 + process: 0.11.10 + rsbuild-plugin-html-minifier-terser: 1.1.2(@rsbuild/core@1.7.2) + sirv: 2.0.4 + storybook: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) + ts-dedent: 2.2.0 + url: 0.11.4 + util: 0.12.5 + util-deprecate: 1.0.2 + optionalDependencies: + react: 19.2.3 + react-dom: 19.2.3(react@19.2.3) + typescript: 5.9.3 + transitivePeerDependencies: + - '@rspack/core' + - msw + - vite + + storybook-react-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1(esbuild@0.27.2)): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.53.3) '@rsbuild/core': 1.7.1 @@ -31034,7 +31759,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) resolve: 1.22.11 storybook: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) tsconfig-paths: 4.2.0 optionalDependencies: typescript: 5.9.3 @@ -31046,10 +31771,10 @@ snapshots: - vite - webpack - storybook-react-rsbuild@3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1): + storybook-react-rsbuild@3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(rollup@4.53.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(webpack@5.104.1): dependencies: '@rollup/pluginutils': 5.3.0(rollup@4.53.3) - '@rsbuild/core': 1.7.1 + '@rsbuild/core': 1.7.2 '@storybook/react': 10.1.10(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3) '@storybook/react-docgen-typescript-plugin': 1.0.1(typescript@5.9.3)(webpack@5.104.1) find-up: 5.0.0 @@ -31060,7 +31785,7 @@ snapshots: react-dom: 19.2.3(react@19.2.3) resolve: 1.22.11 storybook: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.2)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) tsconfig-paths: 4.2.0 optionalDependencies: typescript: 5.9.3 @@ -31072,12 +31797,12 @@ snapshots: - vite - webpack - storybook-vue3-rsbuild@3.2.0(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.3))(webpack@5.104.1(esbuild@0.27.2)): + storybook-vue3-rsbuild@3.2.0(@babel/preset-env@7.28.5(@babel/core@7.28.5))(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1))(vue@3.5.21(typescript@5.9.3))(webpack@5.104.1(esbuild@0.27.2)): dependencies: '@rsbuild/core': 1.7.1 '@storybook/vue3': 10.1.11(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(vue@3.5.21(typescript@5.9.3)) storybook: 10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3) - storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.0(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) + storybook-builder-rsbuild: 3.2.0(@rsbuild/core@1.7.1)(@rspack/core@1.7.1(@swc/helpers@0.5.18))(react-dom@19.2.3(react@19.2.3))(react@19.2.3)(storybook@10.1.11(@testing-library/dom@10.4.0)(prettier@2.8.8)(react-dom@19.2.3(react@19.2.3))(react@19.2.3))(typescript@5.9.3)(vite@7.2.6(@types/node@25.0.3)(jiti@2.6.1)(less@4.5.1)(lightningcss@1.30.1)(sass-embedded@1.93.2)(sass@1.97.1)(stylus@0.64.0)(terser@5.37.0)(tsx@4.19.2)(yaml@2.8.1)) vue: 3.5.21(typescript@5.9.3) vue-docgen-api: 4.79.2(vue@3.5.21(typescript@5.9.3)) vue-docgen-loader: 2.0.1(@babel/preset-env@7.28.5(@babel/core@7.28.5))(vue-docgen-api@4.79.2(vue@3.5.21(typescript@5.9.3)))(webpack@5.104.1(esbuild@0.27.2)) @@ -31462,6 +32187,8 @@ snapshots: csso: 5.0.5 picocolors: 1.1.1 + swc-plugin-coverage-instrument@0.0.32: {} + swc-plugin-css-modules@8.0.0: {} swc-plugin-vue-jsx@0.4.0(@swc/core@1.15.8(@swc/helpers@0.5.18)): @@ -31756,6 +32483,19 @@ snapshots: optionalDependencies: '@rspack/core': 1.7.0(@swc/helpers@0.5.18) + ts-checker-rspack-plugin@1.2.3(@rspack/core@1.7.1(@swc/helpers@0.5.18))(typescript@5.9.3): + dependencies: + '@babel/code-frame': 7.27.1 + '@rspack/lite-tapable': 1.1.0 + chokidar: 3.6.0 + is-glob: 4.0.3 + memfs: 4.51.1 + minimatch: 9.0.5 + picocolors: 1.1.1 + typescript: 5.9.3 + optionalDependencies: + '@rspack/core': 1.7.1(@swc/helpers@0.5.18) + ts-dedent@2.2.0: {} ts-interface-checker@0.1.13: {} @@ -31936,6 +32676,8 @@ snapshots: quansync: 1.0.0 unconfig-core: 7.4.2 + undici-types@6.21.0: {} + undici-types@7.16.0: {} unhead@2.0.19: @@ -32267,7 +33009,7 @@ snapshots: v8-to-istanbul@9.2.0: dependencies: - '@jridgewell/trace-mapping': 0.3.29 + '@jridgewell/trace-mapping': 0.3.31 '@types/istanbul-lib-coverage': 2.0.6 convert-source-map: 2.0.0 @@ -32512,6 +33254,72 @@ snapshots: - walrus - whiskers + vue-loader@15.11.1(@vue/compiler-sfc@3.5.24)(css-loader@7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1))(webpack@5.104.1): + dependencies: + '@vue/component-compiler-utils': 3.3.0(ejs@3.1.10)(handlebars@4.7.8)(lodash@4.17.21)(pug@3.0.2) + css-loader: 7.1.2(@rspack/core@1.7.1(@swc/helpers@0.5.18))(webpack@5.104.1) + hash-sum: 1.0.2 + loader-utils: 1.4.2 + vue-hot-reload-api: 2.3.4 + vue-style-loader: 4.1.3 + webpack: 5.104.1(webpack-cli@6.0.1) + optionalDependencies: + '@vue/compiler-sfc': 3.5.24 + transitivePeerDependencies: + - arc-templates + - atpl + - babel-core + - bracket-template + - coffee-script + - dot + - dust + - dustjs-helpers + - dustjs-linkedin + - eco + - ect + - ejs + - haml-coffee + - hamlet + - hamljs + - handlebars + - hogan.js + - htmling + - jade + - jazz + - jqtpl + - just + - liquid-node + - liquor + - lodash + - marko + - mote + - mustache + - nunjucks + - plates + - pug + - qejs + - ractive + - razor-tmpl + - react + - react-dom + - slm + - squirrelly + - swig + - swig-templates + - teacup + - templayed + - then-jade + - then-pug + - tinyliquid + - toffee + - twig + - twing + - underscore + - vash + - velocityjs + - walrus + - whiskers + vue-loader@17.4.2(vue@3.2.45)(webpack@5.104.1): dependencies: chalk: 4.1.2 @@ -32864,6 +33672,8 @@ snapshots: dependencies: iconv-lite: 0.6.3 + whatwg-mimetype@3.0.0: {} + whatwg-mimetype@4.0.0: {} whatwg-url@14.2.0: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index e54caeb4..e79c588c 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -4,4 +4,5 @@ packages: - "rslib/**" - "rspress/**" - "rsdoctor/**" + - "rstest/**" - "!**/dist" diff --git a/rstest/browser-rsbuild-react/package.json b/rstest/browser-rsbuild-react/package.json new file mode 100644 index 00000000..bb677818 --- /dev/null +++ b/rstest/browser-rsbuild-react/package.json @@ -0,0 +1,26 @@ +{ + "name": "rstest-browser-rsbuild-react", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "test": "rstest run" + }, + "devDependencies": { + "@rsbuild/core": "^1.4.2", + "@rsbuild/plugin-react": "^1.4.2", + "@rstest/adapter-rsbuild": "^0.1.0", + "@rstest/browser": "^0.7.9", + "@rstest/browser-react": "^0.7.9", + "@rstest/core": "^0.7.9", + "@testing-library/dom": "^10.4.0", + "@testing-library/user-event": "^14.6.1", + "@types/react": "^19.1.8", + "@types/react-dom": "^19.1.6", + "playwright": "^1.57.0", + "react": "^19.2.3", + "react-dom": "^19.2.3", + "typescript": "^5.8.3" + } +} diff --git a/rstest/browser-rsbuild-react/rsbuild.config.ts b/rstest/browser-rsbuild-react/rsbuild.config.ts new file mode 100644 index 00000000..1e6583b3 --- /dev/null +++ b/rstest/browser-rsbuild-react/rsbuild.config.ts @@ -0,0 +1,19 @@ +import path from 'node:path'; +import { defineConfig } from '@rsbuild/core'; +import { pluginReact } from '@rsbuild/plugin-react'; + +export default defineConfig({ + plugins: [pluginReact()], + source: { + entry: { + index: './src/index.tsx', + }, + alias: { + '@': path.resolve(__dirname, 'src'), + '@components': path.resolve(__dirname, 'src/components'), + }, + define: { + __APP_VERSION__: JSON.stringify('1.0.0'), + }, + }, +}); diff --git a/rstest/browser-rsbuild-react/rstest.config.ts b/rstest/browser-rsbuild-react/rstest.config.ts new file mode 100644 index 00000000..39804db1 --- /dev/null +++ b/rstest/browser-rsbuild-react/rstest.config.ts @@ -0,0 +1,11 @@ +import { withRsbuildConfig } from '@rstest/adapter-rsbuild'; +import { defineConfig, type ExtendConfigFn } from '@rstest/core'; + +export default defineConfig({ + extends: withRsbuildConfig() as ExtendConfigFn, + browser: { + enabled: true, + browser: 'chromium', + port: 3012, + }, +}); diff --git a/rstest/browser-rsbuild-react/src/components/Counter.tsx b/rstest/browser-rsbuild-react/src/components/Counter.tsx new file mode 100644 index 00000000..c650ecc4 --- /dev/null +++ b/rstest/browser-rsbuild-react/src/components/Counter.tsx @@ -0,0 +1,40 @@ +import { useState } from 'react'; + +export interface CounterProps { + initialValue?: number; + step?: number; +} + +export function Counter({ initialValue = 0, step = 1 }: CounterProps) { + const [count, setCount] = useState(initialValue); + + return ( +
+ + {count} + + +
+ ); +} diff --git a/rstest/browser-rsbuild-react/src/hooks.ts b/rstest/browser-rsbuild-react/src/hooks.ts new file mode 100644 index 00000000..edb6375d --- /dev/null +++ b/rstest/browser-rsbuild-react/src/hooks.ts @@ -0,0 +1,25 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; + +export function useCounter(initialValue = 0, step = 1) { + const [count, setCount] = useState(initialValue); + + const increment = useCallback(() => setCount((c) => c + step), [step]); + const decrement = useCallback(() => setCount((c) => c - step), [step]); + const reset = useCallback(() => setCount(initialValue), [initialValue]); + + return { count, increment, decrement, reset }; +} + +export function useToggle(initialValue = false) { + const [value, setValue] = useState(initialValue); + const toggle = useCallback(() => setValue((v) => !v), []); + return { value, toggle }; +} + +export function usePrevious(value: T): T | undefined { + const ref = useRef(undefined); + useEffect(() => { + ref.current = value; + }, [value]); + return ref.current; +} diff --git a/rstest/browser-rsbuild-react/src/index.tsx b/rstest/browser-rsbuild-react/src/index.tsx new file mode 100644 index 00000000..50e6769b --- /dev/null +++ b/rstest/browser-rsbuild-react/src/index.tsx @@ -0,0 +1,13 @@ +import { StrictMode } from 'react'; +import { createRoot } from 'react-dom/client'; +import { Counter } from './components/Counter'; + +const root = document.getElementById('root'); +if (root) { + createRoot(root).render( + +

React Counter App

+ +
, + ); +} diff --git a/rstest/browser-rsbuild-react/tests/adapter.test.tsx b/rstest/browser-rsbuild-react/tests/adapter.test.tsx new file mode 100644 index 00000000..9f17825a --- /dev/null +++ b/rstest/browser-rsbuild-react/tests/adapter.test.tsx @@ -0,0 +1,30 @@ +// Use @components alias inherited from rsbuild.config.ts +import { Counter } from '@components/Counter'; +import { cleanup, render } from '@rstest/browser-react'; +import { afterEach, describe, expect, it } from '@rstest/core'; +import { getByTestId } from '@testing-library/dom'; + +declare const __APP_VERSION__: string; + +describe('withRsbuildConfig - alias inheritance', () => { + afterEach(() => { + cleanup(); + }); + + it('should resolve @components alias from rsbuild.config.ts', async () => { + const { container } = await render(); + expect(getByTestId(container, 'counter-value').textContent).toBe('5'); + }); + + it('should resolve @/ alias from rsbuild.config.ts', async () => { + // Dynamic import using @/ alias + const { useCounter } = await import('@/hooks'); + expect(typeof useCounter).toBe('function'); + }); +}); + +describe('withRsbuildConfig - define inheritance', () => { + it('should inherit __APP_VERSION__ from rsbuild.config.ts', () => { + expect(__APP_VERSION__).toBe('1.0.0'); + }); +}); diff --git a/rstest/browser-rsbuild-react/tests/counter.test.tsx b/rstest/browser-rsbuild-react/tests/counter.test.tsx new file mode 100644 index 00000000..e36d997b --- /dev/null +++ b/rstest/browser-rsbuild-react/tests/counter.test.tsx @@ -0,0 +1,33 @@ +import { act, cleanup, render } from '@rstest/browser-react'; +import { afterEach, describe, expect, it } from '@rstest/core'; +import { getByTestId } from '@testing-library/dom'; +import userEvent from '@testing-library/user-event'; +import { Counter } from '../src/components/Counter'; + +describe('render - Component Testing', () => { + afterEach(() => { + cleanup(); + }); + + it('should render with initial value', async () => { + const { container } = await render(); + expect(getByTestId(container, 'counter-value').textContent).toBe('10'); + }); + + it('should increment on button click', async () => { + const user = userEvent.setup(); + const { container } = await render(); + + await act(() => user.click(getByTestId(container, 'increment-btn'))); + + expect(getByTestId(container, 'counter-value').textContent).toBe('1'); + }); + + it('should clean up on unmount', async () => { + const { container, unmount } = await render(); + expect(container.querySelector('[data-testid="counter"]')).not.toBeNull(); + + await unmount(); + expect(container.innerHTML).toBe(''); + }); +}); diff --git a/rstest/browser-rsbuild-react/tests/hooks.test.ts b/rstest/browser-rsbuild-react/tests/hooks.test.ts new file mode 100644 index 00000000..8110ecfd --- /dev/null +++ b/rstest/browser-rsbuild-react/tests/hooks.test.ts @@ -0,0 +1,55 @@ +import { act, cleanup, renderHook } from '@rstest/browser-react'; +import { afterEach, describe, expect, it } from '@rstest/core'; +import { useCounter, usePrevious, useToggle } from '../src/hooks'; + +describe('renderHook - useCounter', () => { + afterEach(() => { + cleanup(); + }); + + it('should initialize with value', async () => { + const { result } = await renderHook(() => useCounter(10)); + expect(result.current.count).toBe(10); + }); + + it('should increment with act', async () => { + const { result } = await renderHook(() => useCounter(0)); + + await act(() => result.current.increment()); + + expect(result.current.count).toBe(1); + }); +}); + +describe('renderHook - useToggle', () => { + afterEach(() => { + cleanup(); + }); + + it('should toggle value', async () => { + const { result } = await renderHook(() => useToggle(false)); + + await act(() => result.current.toggle()); + expect(result.current.value).toBe(true); + + await act(() => result.current.toggle()); + expect(result.current.value).toBe(false); + }); +}); + +describe('renderHook - usePrevious', () => { + afterEach(() => { + cleanup(); + }); + + it('should return previous value after rerender', async () => { + let value = 1; + const { result, rerender } = await renderHook(() => usePrevious(value)); + + expect(result.current).toBeUndefined(); + + value = 2; + await rerender(); + expect(result.current).toBe(1); + }); +}); diff --git a/rstest/browser-rsbuild-react/tsconfig.json b/rstest/browser-rsbuild-react/tsconfig.json new file mode 100644 index 00000000..e039ed87 --- /dev/null +++ b/rstest/browser-rsbuild-react/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "jsx": "react-jsx", + "types": ["@rstest/core"], + "baseUrl": ".", + "paths": { + "@/*": ["src/*"], + "@components/*": ["src/components/*"] + } + }, + "include": ["src", "tests", "rstest.config.ts", "rsbuild.config.ts"] +} diff --git a/rstest/browser-rsbuild-vanilla/package.json b/rstest/browser-rsbuild-vanilla/package.json new file mode 100644 index 00000000..22d7ff1e --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/package.json @@ -0,0 +1,18 @@ +{ + "name": "rstest-browser-rsbuild-vanilla", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "test": "rstest run" + }, + "devDependencies": { + "@rsbuild/core": "^1.7.1", + "@rstest/browser": "^0.7.9", + "@rstest/core": "^0.7.9", + "playwright": "^1.57.0", + "typescript": "^5.8.3" + } +} diff --git a/rstest/browser-rsbuild-vanilla/rsbuild.config.ts b/rstest/browser-rsbuild-vanilla/rsbuild.config.ts new file mode 100644 index 00000000..a85d2ff0 --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/rsbuild.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@rsbuild/core'; + +export default defineConfig({ + source: { + entry: { + index: './src/index.ts', + }, + }, +}); diff --git a/rstest/browser-rsbuild-vanilla/rstest.config.ts b/rstest/browser-rsbuild-vanilla/rstest.config.ts new file mode 100644 index 00000000..165728c4 --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/rstest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + browser: { + enabled: true, + browser: 'chromium', + port: 3010, + }, +}); diff --git a/rstest/browser-rsbuild-vanilla/src/counter.ts b/rstest/browser-rsbuild-vanilla/src/counter.ts new file mode 100644 index 00000000..9e2b9278 --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/src/counter.ts @@ -0,0 +1,47 @@ +export function createCounter(initialValue = 0) { + let value = initialValue; + + const container = document.createElement('div'); + container.className = 'counter'; + + const display = document.createElement('span'); + display.className = 'counter-display'; + display.textContent = String(value); + display.setAttribute('data-testid', 'counter-value'); + + const incrementBtn = document.createElement('button'); + incrementBtn.textContent = '+'; + incrementBtn.setAttribute('data-testid', 'increment-btn'); + + const decrementBtn = document.createElement('button'); + decrementBtn.textContent = '-'; + decrementBtn.setAttribute('data-testid', 'decrement-btn'); + + const updateDisplay = () => { + display.textContent = String(value); + }; + + const increment = () => { + value++; + updateDisplay(); + }; + + const decrement = () => { + value--; + updateDisplay(); + }; + + incrementBtn.addEventListener('click', increment); + decrementBtn.addEventListener('click', decrement); + + container.appendChild(decrementBtn); + container.appendChild(display); + container.appendChild(incrementBtn); + + return { + element: container, + getValue: () => value, + increment, + decrement, + }; +} diff --git a/rstest/browser-rsbuild-vanilla/src/index.ts b/rstest/browser-rsbuild-vanilla/src/index.ts new file mode 100644 index 00000000..2db7dcbf --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/src/index.ts @@ -0,0 +1,16 @@ +import { createCounter } from './counter'; + +const app = document.querySelector('#root'); + +if (app) { + app.innerHTML = ` +

Vanilla Counter App

+
+ `; + + const container = document.querySelector('#counter-container'); + if (container) { + const counter = createCounter(0); + container.appendChild(counter.element); + } +} diff --git a/rstest/browser-rsbuild-vanilla/tests/counter.test.ts b/rstest/browser-rsbuild-vanilla/tests/counter.test.ts new file mode 100644 index 00000000..1f81a222 --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/tests/counter.test.ts @@ -0,0 +1,56 @@ +import { afterEach, beforeEach, describe, expect, it } from '@rstest/core'; +import { createCounter } from '../src/counter'; + +describe('createCounter', () => { + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + }); + + afterEach(() => { + container.remove(); + }); + + it('should render with initial value', () => { + const counter = createCounter(10); + container.appendChild(counter.element); + + expect(counter.getValue()).toBe(10); + expect(counter.element.querySelector('[data-testid="counter-value"]')?.textContent).toBe('10'); + }); + + it('should increment on button click', () => { + const counter = createCounter(0); + container.appendChild(counter.element); + + const btn = counter.element.querySelector('[data-testid="increment-btn"]') as HTMLButtonElement; + btn.click(); + + expect(counter.getValue()).toBe(1); + }); + + it('should decrement on button click', () => { + const counter = createCounter(5); + container.appendChild(counter.element); + + const btn = counter.element.querySelector('[data-testid="decrement-btn"]') as HTMLButtonElement; + btn.click(); + + expect(counter.getValue()).toBe(4); + }); +}); + +describe('Browser APIs', () => { + it('should have access to DOM', () => { + expect(typeof document).toBe('object'); + expect(typeof window).toBe('object'); + }); + + it('should have access to localStorage', () => { + localStorage.setItem('test', 'value'); + expect(localStorage.getItem('test')).toBe('value'); + localStorage.removeItem('test'); + }); +}); diff --git a/rstest/browser-rsbuild-vanilla/tsconfig.json b/rstest/browser-rsbuild-vanilla/tsconfig.json new file mode 100644 index 00000000..2156e6d3 --- /dev/null +++ b/rstest/browser-rsbuild-vanilla/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts", "rsbuild.config.ts"] +} diff --git a/rstest/coverage/package.json b/rstest/coverage/package.json new file mode 100644 index 00000000..e5d7ae63 --- /dev/null +++ b/rstest/coverage/package.json @@ -0,0 +1,16 @@ +{ + "name": "rstest-coverage", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "rstest run", + "test:coverage": "rstest run --coverage", + "test:coverage:html": "rstest run --coverage --coverage.reporter=html", + "test:coverage:lcov": "rstest run --coverage --coverage.reporter=lcov" + }, + "devDependencies": { + "@rstest/core": "^0.7.9", + "@rstest/coverage-istanbul": "^0.1.6", + "typescript": "^5.8.3" + } +} diff --git a/rstest/coverage/rstest.config.ts b/rstest/coverage/rstest.config.ts new file mode 100644 index 00000000..0c23f474 --- /dev/null +++ b/rstest/coverage/rstest.config.ts @@ -0,0 +1,19 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + coverage: { + enabled: true, + provider: 'istanbul', + include: ['src/**/*.ts'], + exclude: ['src/**/*.d.ts', 'src/**/*.test.ts', 'src/**/index.ts'], + reporters: ['text', 'html', 'lcov'], + reportsDirectory: './coverage', + thresholds: { + lines: 70, + branches: 70, + functions: 70, + statements: 70, + }, + clean: true, + }, +}); diff --git a/rstest/coverage/src/math.ts b/rstest/coverage/src/math.ts new file mode 100644 index 00000000..ccf28650 --- /dev/null +++ b/rstest/coverage/src/math.ts @@ -0,0 +1,49 @@ +/** + * Basic math utilities demonstrating line and function coverage + */ + +export function add(a: number, b: number): number { + return a + b; +} + +export function subtract(a: number, b: number): number { + return a - b; +} + +export function multiply(a: number, b: number): number { + return a * b; +} + +export function divide(a: number, b: number): number { + if (b === 0) { + throw new Error('Cannot divide by zero'); + } + return a / b; +} + +/** + * Calculate the factorial of a number + * Demonstrates branch coverage with recursion + */ +export function factorial(n: number): number { + if (n < 0) { + throw new Error('Cannot calculate factorial of negative number'); + } + if (n === 0 || n === 1) { + return 1; + } + return n * factorial(n - 1); +} + +/** + * This function is intentionally NOT tested to show uncovered code + */ +export function untested_power(base: number, exponent: number): number { + if (exponent === 0) { + return 1; + } + if (exponent < 0) { + return 1 / untested_power(base, -exponent); + } + return base * untested_power(base, exponent - 1); +} diff --git a/rstest/coverage/src/validator.ts b/rstest/coverage/src/validator.ts new file mode 100644 index 00000000..044ed456 --- /dev/null +++ b/rstest/coverage/src/validator.ts @@ -0,0 +1,120 @@ +/** + * Validation utilities demonstrating branch coverage + */ + +export interface ValidationResult { + valid: boolean; + errors: string[]; +} + +/** + * Validate an email address + * Multiple branches for different validation rules + */ +export function validateEmail(email: string): ValidationResult { + const errors: string[] = []; + + if (!email) { + errors.push('Email is required'); + return { valid: false, errors }; + } + + if (!email.includes('@')) { + errors.push('Email must contain @'); + } + + if (!email.includes('.')) { + errors.push('Email must contain a domain'); + } + + const [local, domain] = email.split('@'); + + if (local && local.length > 64) { + errors.push('Local part too long'); + } + + if (domain && domain.length > 255) { + errors.push('Domain too long'); + } + + return { + valid: errors.length === 0, + errors, + }; +} + +/** + * Validate a password with multiple criteria + */ +export function validatePassword(password: string): ValidationResult { + const errors: string[] = []; + + if (!password) { + errors.push('Password is required'); + return { valid: false, errors }; + } + + if (password.length < 8) { + errors.push('Password must be at least 8 characters'); + } + + if (password.length > 128) { + errors.push('Password must be at most 128 characters'); + } + + if (!/[A-Z]/.test(password)) { + errors.push('Password must contain an uppercase letter'); + } + + if (!/[a-z]/.test(password)) { + errors.push('Password must contain a lowercase letter'); + } + + if (!/[0-9]/.test(password)) { + errors.push('Password must contain a number'); + } + + if (!/[!@#$%^&*]/.test(password)) { + errors.push('Password must contain a special character'); + } + + return { + valid: errors.length === 0, + errors, + }; +} + +/** + * This function has branches that are intentionally not fully tested + * to demonstrate partial branch coverage + */ +export function validateAge(age: unknown): ValidationResult { + const errors: string[] = []; + + if (age === null || age === undefined) { + errors.push('Age is required'); + return { valid: false, errors }; + } + + if (typeof age !== 'number') { + errors.push('Age must be a number'); + return { valid: false, errors }; + } + + if (!Number.isInteger(age)) { + errors.push('Age must be an integer'); + } + + if (age < 0) { + errors.push('Age cannot be negative'); + } + + if (age > 150) { + errors.push('Age seems unrealistic'); + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/rstest/coverage/tests/math.test.ts b/rstest/coverage/tests/math.test.ts new file mode 100644 index 00000000..cc592afb --- /dev/null +++ b/rstest/coverage/tests/math.test.ts @@ -0,0 +1,74 @@ +import { describe, expect, it } from '@rstest/core'; +import { add, divide, factorial, multiply, subtract } from '../src/math'; + +describe('math utilities', () => { + describe('add', () => { + it('should add two positive numbers', () => { + expect(add(2, 3)).toBe(5); + }); + + it('should add negative numbers', () => { + expect(add(-2, -3)).toBe(-5); + }); + + it('should add zero', () => { + expect(add(5, 0)).toBe(5); + }); + }); + + describe('subtract', () => { + it('should subtract two numbers', () => { + expect(subtract(5, 3)).toBe(2); + }); + + it('should handle negative result', () => { + expect(subtract(3, 5)).toBe(-2); + }); + }); + + describe('multiply', () => { + it('should multiply two numbers', () => { + expect(multiply(4, 5)).toBe(20); + }); + + it('should handle zero', () => { + expect(multiply(4, 0)).toBe(0); + }); + }); + + describe('divide', () => { + it('should divide two numbers', () => { + expect(divide(10, 2)).toBe(5); + }); + + it('should throw error when dividing by zero', () => { + expect(() => divide(10, 0)).toThrow('Cannot divide by zero'); + }); + + it('should handle decimal results', () => { + expect(divide(5, 2)).toBe(2.5); + }); + }); + + describe('factorial', () => { + it('should return 1 for factorial of 0', () => { + expect(factorial(0)).toBe(1); + }); + + it('should return 1 for factorial of 1', () => { + expect(factorial(1)).toBe(1); + }); + + it('should calculate factorial of positive numbers', () => { + expect(factorial(5)).toBe(120); + expect(factorial(3)).toBe(6); + }); + + it('should throw error for negative numbers', () => { + expect(() => factorial(-1)).toThrow('Cannot calculate factorial of negative number'); + }); + }); + + // Note: untested_power is intentionally NOT tested + // to demonstrate uncovered code in coverage reports +}); diff --git a/rstest/coverage/tests/validator.test.ts b/rstest/coverage/tests/validator.test.ts new file mode 100644 index 00000000..d3cc6683 --- /dev/null +++ b/rstest/coverage/tests/validator.test.ts @@ -0,0 +1,117 @@ +import { describe, expect, it } from '@rstest/core'; +import { validateAge, validateEmail, validatePassword } from '../src/validator'; + +describe('validator utilities', () => { + describe('validateEmail', () => { + it('should return error for empty email', () => { + const result = validateEmail(''); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Email is required'); + }); + + it('should return error for email without @', () => { + const result = validateEmail('invalidemail.com'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Email must contain @'); + }); + + it('should return error for email without domain dot', () => { + const result = validateEmail('test@localhost'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Email must contain a domain'); + }); + + it('should validate correct email', () => { + const result = validateEmail('test@example.com'); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + it('should return error for local part too long', () => { + const longLocal = 'a'.repeat(65); + const result = validateEmail(`${longLocal}@example.com`); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Local part too long'); + }); + + // Note: Domain too long branch is intentionally not tested + // to show partial branch coverage + }); + + describe('validatePassword', () => { + it('should return error for empty password', () => { + const result = validatePassword(''); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password is required'); + }); + + it('should return error for short password', () => { + const result = validatePassword('Ab1!'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password must be at least 8 characters'); + }); + + it('should return error for password without uppercase', () => { + const result = validatePassword('password1!'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password must contain an uppercase letter'); + }); + + it('should return error for password without lowercase', () => { + const result = validatePassword('PASSWORD1!'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password must contain a lowercase letter'); + }); + + it('should return error for password without number', () => { + const result = validatePassword('Password!!'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password must contain a number'); + }); + + it('should return error for password without special character', () => { + const result = validatePassword('Password1a'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Password must contain a special character'); + }); + + it('should validate correct password', () => { + const result = validatePassword('SecureP@ss1'); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + // Note: Password too long branch is intentionally not tested + }); + + describe('validateAge', () => { + it('should return error for null age', () => { + const result = validateAge(null); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Age is required'); + }); + + it('should return error for undefined age', () => { + const result = validateAge(undefined); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Age is required'); + }); + + it('should return error for non-number age', () => { + const result = validateAge('25'); + expect(result.valid).toBe(false); + expect(result.errors).toContain('Age must be a number'); + }); + + it('should validate correct age', () => { + const result = validateAge(25); + expect(result.valid).toBe(true); + expect(result.errors).toHaveLength(0); + }); + + // Note: Following branches are intentionally not tested: + // - Non-integer age + // - Negative age + // - Age > 150 + }); +}); diff --git a/rstest/coverage/tsconfig.json b/rstest/coverage/tsconfig.json new file mode 100644 index 00000000..5d1c2508 --- /dev/null +++ b/rstest/coverage/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts"] +} diff --git a/rstest/fake-timers/package.json b/rstest/fake-timers/package.json new file mode 100644 index 00000000..09d6bb65 --- /dev/null +++ b/rstest/fake-timers/package.json @@ -0,0 +1,12 @@ +{ + "name": "rstest-fake-timers", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "rstest run" + }, + "devDependencies": { + "@rstest/core": "^0.7.9", + "typescript": "^5.8.3" + } +} diff --git a/rstest/fake-timers/rstest.config.ts b/rstest/fake-timers/rstest.config.ts new file mode 100644 index 00000000..9ee3cbab --- /dev/null +++ b/rstest/fake-timers/rstest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({}); diff --git a/rstest/fake-timers/src/index.ts b/rstest/fake-timers/src/index.ts new file mode 100644 index 00000000..46eba0e5 --- /dev/null +++ b/rstest/fake-timers/src/index.ts @@ -0,0 +1,3 @@ +export * from './polling'; +export * from './timer'; +export * from './timing'; diff --git a/rstest/fake-timers/src/polling.ts b/rstest/fake-timers/src/polling.ts new file mode 100644 index 00000000..1bcbcaf6 --- /dev/null +++ b/rstest/fake-timers/src/polling.ts @@ -0,0 +1,127 @@ +/** + * Polling utility for checking conditions + */ +export class Poller { + private intervalId: ReturnType | null = null; + private timeoutId: ReturnType | null = null; + + /** + * Poll until condition is met or timeout + */ + async poll( + checkFn: () => T | Promise, + options: { + interval?: number; + timeout?: number; + condition?: (result: T) => boolean; + } = {}, + ): Promise { + const { interval = 100, timeout = 5000, condition = Boolean } = options; + + return new Promise((resolve, reject) => { + const startTime = Date.now(); + + const check = async () => { + try { + const result = await checkFn(); + + if (condition(result)) { + this.stop(); + resolve(result); + } else if (Date.now() - startTime >= timeout) { + this.stop(); + reject(new Error(`Polling timeout after ${timeout}ms`)); + } + } catch (error) { + this.stop(); + reject(error); + } + }; + + // Set timeout for overall polling + this.timeoutId = setTimeout(() => { + this.stop(); + reject(new Error(`Polling timeout after ${timeout}ms`)); + }, timeout); + + // Start polling + this.intervalId = setInterval(check, interval); + + // Check immediately + check(); + }); + } + + /** + * Stop polling + */ + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + if (this.timeoutId) { + clearTimeout(this.timeoutId); + this.timeoutId = null; + } + } +} + +/** + * Rate limiter using token bucket algorithm + */ +export class RateLimiter { + private tokens: number; + private lastRefill: number; + private readonly maxTokens: number; + private readonly refillRate: number; // tokens per second + + constructor(maxTokens: number, refillRate: number) { + this.maxTokens = maxTokens; + this.tokens = maxTokens; + this.refillRate = refillRate; + this.lastRefill = Date.now(); + } + + /** + * Try to acquire a token + */ + tryAcquire(): boolean { + this.refill(); + + if (this.tokens >= 1) { + this.tokens -= 1; + return true; + } + + return false; + } + + /** + * Get time until next token is available + */ + getWaitTime(): number { + this.refill(); + + if (this.tokens >= 1) { + return 0; + } + + return Math.ceil(((1 - this.tokens) / this.refillRate) * 1000); + } + + /** + * Get current token count + */ + getTokens(): number { + this.refill(); + return Math.floor(this.tokens); + } + + private refill(): void { + const now = Date.now(); + const elapsed = (now - this.lastRefill) / 1000; + this.tokens = Math.min(this.maxTokens, this.tokens + elapsed * this.refillRate); + this.lastRefill = now; + } +} diff --git a/rstest/fake-timers/src/timer.ts b/rstest/fake-timers/src/timer.ts new file mode 100644 index 00000000..02c49111 --- /dev/null +++ b/rstest/fake-timers/src/timer.ts @@ -0,0 +1,127 @@ +/** + * Countdown timer + */ +export class CountdownTimer { + private remaining: number; + private intervalId: ReturnType | null = null; + private onTick?: (remaining: number) => void; + private onComplete?: () => void; + + constructor(durationMs: number) { + this.remaining = durationMs; + } + + /** + * Set callback for each tick + */ + setOnTick(callback: (remaining: number) => void): void { + this.onTick = callback; + } + + /** + * Set callback for when timer completes + */ + setOnComplete(callback: () => void): void { + this.onComplete = callback; + } + + /** + * Start the countdown + */ + start(tickInterval = 1000): void { + if (this.intervalId) return; + + this.intervalId = setInterval(() => { + this.remaining -= tickInterval; + + if (this.remaining <= 0) { + this.remaining = 0; + this.stop(); + this.onTick?.(0); + this.onComplete?.(); + } else { + this.onTick?.(this.remaining); + } + }, tickInterval); + } + + /** + * Stop the countdown + */ + stop(): void { + if (this.intervalId) { + clearInterval(this.intervalId); + this.intervalId = null; + } + } + + /** + * Get remaining time + */ + getRemaining(): number { + return this.remaining; + } + + /** + * Check if timer is running + */ + isRunning(): boolean { + return this.intervalId !== null; + } +} + +/** + * Stopwatch for measuring elapsed time + */ +export class Stopwatch { + private startTime: number | null = null; + private elapsedTime = 0; + private running = false; + + /** + * Start the stopwatch + */ + start(): void { + if (!this.running) { + this.startTime = Date.now(); + this.running = true; + } + } + + /** + * Stop the stopwatch + */ + stop(): void { + if (this.running && this.startTime !== null) { + this.elapsedTime += Date.now() - this.startTime; + this.running = false; + this.startTime = null; + } + } + + /** + * Reset the stopwatch + */ + reset(): void { + this.startTime = null; + this.elapsedTime = 0; + this.running = false; + } + + /** + * Get elapsed time in milliseconds + */ + getElapsed(): number { + if (this.running && this.startTime !== null) { + return this.elapsedTime + (Date.now() - this.startTime); + } + return this.elapsedTime; + } + + /** + * Check if stopwatch is running + */ + isRunning(): boolean { + return this.running; + } +} diff --git a/rstest/fake-timers/src/timing.ts b/rstest/fake-timers/src/timing.ts new file mode 100644 index 00000000..cb247fec --- /dev/null +++ b/rstest/fake-timers/src/timing.ts @@ -0,0 +1,100 @@ +/** + * Debounce function - delays execution until after wait ms since last call + */ +export function debounce unknown>( + fn: T, + wait: number, +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + fn(...args); + timeoutId = null; + }, wait); + }; +} + +/** + * Throttle function - limits execution to once per wait ms + */ +export function throttle unknown>( + fn: T, + wait: number, +): (...args: Parameters) => void { + let lastCall = 0; + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + const now = Date.now(); + const remaining = wait - (now - lastCall); + + if (remaining <= 0) { + if (timeoutId) { + clearTimeout(timeoutId); + timeoutId = null; + } + lastCall = now; + fn(...args); + } else if (!timeoutId) { + timeoutId = setTimeout(() => { + lastCall = Date.now(); + timeoutId = null; + fn(...args); + }, remaining); + } + }; +} + +/** + * Retry function with exponential backoff + */ +export async function retryWithBackoff( + fn: () => Promise, + options: { + maxRetries?: number; + initialDelay?: number; + maxDelay?: number; + factor?: number; + } = {}, +): Promise { + const { maxRetries = 3, initialDelay = 100, maxDelay = 5000, factor = 2 } = options; + + let lastError: Error | null = null; + let delay = initialDelay; + + for (let attempt = 0; attempt <= maxRetries; attempt++) { + try { + return await fn(); + } catch (error) { + lastError = error as Error; + + if (attempt < maxRetries) { + await new Promise((resolve) => setTimeout(resolve, delay)); + delay = Math.min(delay * factor, maxDelay); + } + } + } + + throw lastError; +} + +/** + * Delay helper + */ +export function delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Timeout helper - rejects if operation takes too long + */ +export function withTimeout(promise: Promise, ms: number): Promise { + return Promise.race([ + promise, + new Promise((_, reject) => setTimeout(() => reject(new Error(`Timeout after ${ms}ms`)), ms)), + ]); +} diff --git a/rstest/fake-timers/tests/advanced.test.ts b/rstest/fake-timers/tests/advanced.test.ts new file mode 100644 index 00000000..2dfa7f13 --- /dev/null +++ b/rstest/fake-timers/tests/advanced.test.ts @@ -0,0 +1,143 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; +import { RateLimiter } from '../src/polling'; +import { retryWithBackoff } from '../src/timing'; + +describe('RateLimiter with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + rstest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should allow requests up to max tokens', () => { + const limiter = new RateLimiter(3, 1); + + expect(limiter.tryAcquire()).toBe(true); + expect(limiter.tryAcquire()).toBe(true); + expect(limiter.tryAcquire()).toBe(true); + expect(limiter.tryAcquire()).toBe(false); + }); + + it('should refill tokens over time', () => { + const limiter = new RateLimiter(3, 1); // 1 token per second + + limiter.tryAcquire(); + limiter.tryAcquire(); + limiter.tryAcquire(); + expect(limiter.getTokens()).toBe(0); + + rstest.advanceTimersByTime(2000); + + expect(limiter.getTokens()).toBe(2); + expect(limiter.tryAcquire()).toBe(true); + expect(limiter.tryAcquire()).toBe(true); + }); + + it('should not exceed max tokens', () => { + const limiter = new RateLimiter(3, 1); + + rstest.advanceTimersByTime(10000); + + expect(limiter.getTokens()).toBe(3); + }); + + it('should calculate wait time correctly', () => { + const limiter = new RateLimiter(1, 1); + + limiter.tryAcquire(); + expect(limiter.getWaitTime()).toBeGreaterThan(0); + + rstest.advanceTimersByTime(1000); + expect(limiter.getWaitTime()).toBe(0); + }); +}); + +describe('retryWithBackoff with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should succeed on first attempt', async () => { + const fn = rstest.fn().mockResolvedValue('success'); + + const resultPromise = retryWithBackoff(fn, { maxRetries: 3 }); + + await rstest.runAllTimersAsync(); + + const result = await resultPromise; + expect(result).toBe('success'); + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should retry on failure and eventually succeed', async () => { + const fn = rstest.fn().mockRejectedValueOnce(new Error('fail 1')).mockResolvedValue('success'); + + const resultPromise = retryWithBackoff(fn, { + maxRetries: 3, + initialDelay: 100, + }); + + // Run all timers to complete the retry cycle + await rstest.runAllTimersAsync(); + + const result = await resultPromise; + expect(result).toBe('success'); + expect(fn).toHaveBeenCalledTimes(2); // Initial + 1 retry + }); +}); + +describe('Timeout Patterns with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should handle promise race with timers', async () => { + let resolved = false; + + const slowPromise = new Promise((resolve) => { + setTimeout(() => { + resolved = true; + resolve('done'); + }, 500); + }); + + // Start the promise + slowPromise.then(() => {}); + + expect(resolved).toBe(false); + + await rstest.advanceTimersByTimeAsync(500); + + expect(resolved).toBe(true); + }); + + it('should handle multiple concurrent timers', async () => { + const results: string[] = []; + + setTimeout(() => results.push('first'), 100); + setTimeout(() => results.push('second'), 200); + setTimeout(() => results.push('third'), 300); + + expect(results).toEqual([]); + + await rstest.advanceTimersByTimeAsync(150); + expect(results).toEqual(['first']); + + await rstest.advanceTimersByTimeAsync(100); + expect(results).toEqual(['first', 'second']); + + await rstest.advanceTimersByTimeAsync(100); + expect(results).toEqual(['first', 'second', 'third']); + }); +}); diff --git a/rstest/fake-timers/tests/basic.test.ts b/rstest/fake-timers/tests/basic.test.ts new file mode 100644 index 00000000..a2e7deca --- /dev/null +++ b/rstest/fake-timers/tests/basic.test.ts @@ -0,0 +1,369 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; + +describe('rstest.useFakeTimers() - Basic Usage', () => { + /** + * rstest.useFakeTimers() replaces setTimeout, setInterval, + * Date.now, and other time-related globals with mock implementations + */ + + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + describe('setTimeout', () => { + it('should not execute callback until time advances', () => { + let called = false; + + setTimeout(() => { + called = true; + }, 1000); + + expect(called).toBe(false); + }); + + it('should execute callback after advancing time', () => { + let called = false; + + setTimeout(() => { + called = true; + }, 1000); + + rstest.advanceTimersByTime(1000); + + expect(called).toBe(true); + }); + + it('should not execute if time advanced partially', () => { + let called = false; + + setTimeout(() => { + called = true; + }, 1000); + + rstest.advanceTimersByTime(500); + + expect(called).toBe(false); + + rstest.advanceTimersByTime(500); + + expect(called).toBe(true); + }); + }); + + describe('setInterval', () => { + it('should execute interval callback multiple times', () => { + let count = 0; + + setInterval(() => { + count++; + }, 100); + + rstest.advanceTimersByTime(350); + + expect(count).toBe(3); + }); + + it('should stop interval with clearInterval', () => { + let count = 0; + + const intervalId = setInterval(() => { + count++; + }, 100); + + rstest.advanceTimersByTime(250); + expect(count).toBe(2); + + clearInterval(intervalId); + + rstest.advanceTimersByTime(200); + expect(count).toBe(2); // Should not increase + }); + }); +}); + +describe('rstest.advanceTimersByTime()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should advance time by specified milliseconds', () => { + const results: number[] = []; + + setTimeout(() => results.push(1), 100); + setTimeout(() => results.push(2), 200); + setTimeout(() => results.push(3), 300); + + rstest.advanceTimersByTime(150); + expect(results).toEqual([1]); + + rstest.advanceTimersByTime(100); + expect(results).toEqual([1, 2]); + + rstest.advanceTimersByTime(100); + expect(results).toEqual([1, 2, 3]); + }); + + it('should handle nested timers', () => { + const results: string[] = []; + + setTimeout(() => { + results.push('first'); + setTimeout(() => { + results.push('nested'); + }, 100); + }, 100); + + rstest.advanceTimersByTime(100); + expect(results).toEqual(['first']); + + rstest.advanceTimersByTime(100); + expect(results).toEqual(['first', 'nested']); + }); +}); + +describe('rstest.advanceTimersByTimeAsync()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should advance time and flush promises', async () => { + let resolved = false; + + const promise = new Promise((resolve) => { + setTimeout(() => { + resolved = true; + resolve(); + }, 1000); + }); + + await rstest.advanceTimersByTimeAsync(1000); + + expect(resolved).toBe(true); + await promise; + }); +}); + +describe('rstest.runAllTimers()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should run all pending timers', () => { + const results: number[] = []; + + setTimeout(() => results.push(1), 100); + setTimeout(() => results.push(2), 500); + setTimeout(() => results.push(3), 1000); + + rstest.runAllTimers(); + + expect(results).toEqual([1, 2, 3]); + }); + + it('should run nested timers', () => { + const results: string[] = []; + + setTimeout(() => { + results.push('a'); + setTimeout(() => { + results.push('b'); + setTimeout(() => { + results.push('c'); + }, 100); + }, 100); + }, 100); + + rstest.runAllTimers(); + + expect(results).toEqual(['a', 'b', 'c']); + }); +}); + +describe('rstest.runAllTimersAsync()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should run all timers and flush promises', async () => { + const results: string[] = []; + + setTimeout(async () => { + await Promise.resolve(); + results.push('async-1'); + }, 100); + + setTimeout(async () => { + await Promise.resolve(); + results.push('async-2'); + }, 200); + + await rstest.runAllTimersAsync(); + + expect(results).toEqual(['async-1', 'async-2']); + }); +}); + +describe('rstest.runOnlyPendingTimers()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should only run currently pending timers', () => { + const results: string[] = []; + + setTimeout(() => { + results.push('first'); + setTimeout(() => { + results.push('nested'); + }, 100); + }, 100); + + rstest.runOnlyPendingTimers(); + + // Only first timer runs, nested is not yet pending + expect(results).toEqual(['first']); + + rstest.runOnlyPendingTimers(); + + expect(results).toEqual(['first', 'nested']); + }); + + it('should be useful for interval testing', () => { + let count = 0; + + setInterval(() => { + count++; + }, 100); + + rstest.runOnlyPendingTimers(); + expect(count).toBe(1); + + rstest.runOnlyPendingTimers(); + expect(count).toBe(2); + }); +}); + +describe('rstest.advanceTimersToNextTimer()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should advance to next timer only', () => { + const results: number[] = []; + + setTimeout(() => results.push(1), 100); + setTimeout(() => results.push(2), 200); + setTimeout(() => results.push(3), 300); + + rstest.advanceTimersToNextTimer(); + expect(results).toEqual([1]); + + rstest.advanceTimersToNextTimer(); + expect(results).toEqual([1, 2]); + + rstest.advanceTimersToNextTimer(); + expect(results).toEqual([1, 2, 3]); + }); + + it('should handle multiple steps', () => { + const results: string[] = []; + + setTimeout(() => results.push('a'), 50); + setTimeout(() => results.push('b'), 100); + setTimeout(() => results.push('c'), 150); + + rstest.advanceTimersToNextTimer(2); + + expect(results).toEqual(['a', 'b']); + }); +}); + +describe('rstest.getTimerCount()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should return count of pending timers', () => { + expect(rstest.getTimerCount()).toBe(0); + + setTimeout(() => {}, 100); + expect(rstest.getTimerCount()).toBe(1); + + setTimeout(() => {}, 200); + setTimeout(() => {}, 300); + expect(rstest.getTimerCount()).toBe(3); + + rstest.runAllTimers(); + expect(rstest.getTimerCount()).toBe(0); + }); + + it('should count intervals as one timer each', () => { + const id = setInterval(() => {}, 100); + expect(rstest.getTimerCount()).toBe(1); + + clearInterval(id); + expect(rstest.getTimerCount()).toBe(0); + }); +}); + +describe('rstest.clearAllTimers()', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should clear all pending timers', () => { + let called = false; + + setTimeout(() => { + called = true; + }, 1000); + setInterval(() => { + called = true; + }, 500); + + expect(rstest.getTimerCount()).toBe(2); + + rstest.clearAllTimers(); + + expect(rstest.getTimerCount()).toBe(0); + + rstest.advanceTimersByTime(2000); + expect(called).toBe(false); + }); +}); diff --git a/rstest/fake-timers/tests/date.test.ts b/rstest/fake-timers/tests/date.test.ts new file mode 100644 index 00000000..6b75f294 --- /dev/null +++ b/rstest/fake-timers/tests/date.test.ts @@ -0,0 +1,180 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; + +describe('rstest.setSystemTime() - Mocking Date', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should set system time to specific date', () => { + rstest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + + expect(new Date().toISOString()).toBe('2024-01-01T00:00:00.000Z'); + expect(Date.now()).toBe(new Date('2024-01-01T00:00:00.000Z').getTime()); + }); + + it('should set system time using timestamp', () => { + const timestamp = 1704067200000; // 2024-01-01 + rstest.setSystemTime(timestamp); + + expect(Date.now()).toBe(timestamp); + }); + + it('should advance time from set date', () => { + rstest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + + rstest.advanceTimersByTime(1000 * 60 * 60); // 1 hour + + expect(new Date().toISOString()).toBe('2024-01-01T01:00:00.000Z'); + }); + + it('should work with setTimeout and set date', () => { + rstest.setSystemTime(new Date('2024-06-15T12:00:00.000Z')); + + const captured: { date: Date | null } = { date: null }; + + setTimeout(() => { + captured.date = new Date(); + }, 5000); + + rstest.advanceTimersByTime(5000); + + expect(captured.date?.toISOString()).toBe('2024-06-15T12:00:05.000Z'); + }); +}); + +describe('Verifying Mocked System Time', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should verify mocked time via Date.now()', () => { + rstest.setSystemTime(new Date('2024-01-01T00:00:00.000Z')); + + expect(Date.now()).toBe(new Date('2024-01-01T00:00:00.000Z').getTime()); + + rstest.advanceTimersByTime(1000); + + expect(Date.now()).toBe(new Date('2024-01-01T00:00:01.000Z').getTime()); + }); +}); + +describe('Date Constructor with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + rstest.setSystemTime(new Date('2024-03-15T10:30:00.000Z')); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should create Date with mocked time', () => { + const date = new Date(); + + expect(date.getFullYear()).toBe(2024); + expect(date.getMonth()).toBe(2); // March (0-indexed) + expect(date.getDate()).toBe(15); + expect(date.getUTCHours()).toBe(10); + expect(date.getUTCMinutes()).toBe(30); + }); + + it('should still allow creating specific dates', () => { + const specificDate = new Date('2020-05-20T08:00:00.000Z'); + + expect(specificDate.getFullYear()).toBe(2020); + expect(specificDate.getMonth()).toBe(4); // May + expect(specificDate.getDate()).toBe(20); + }); + + it('should work with date comparisons', () => { + const now = new Date(); + const past = new Date('2024-01-01'); + const future = new Date('2024-12-31'); + + expect(now > past).toBe(true); + expect(now < future).toBe(true); + }); +}); + +describe('Performance.now() with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should mock performance.now()', () => { + const start = performance.now(); + + rstest.advanceTimersByTime(1000); + + const end = performance.now(); + + expect(end - start).toBe(1000); + }); +}); + +describe('Real-world Date Scenarios', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should test expiration logic', () => { + rstest.setSystemTime(new Date('2024-01-15T00:00:00.000Z')); + + const expirationDate = new Date('2024-01-20T00:00:00.000Z'); + + const isExpired = () => new Date() > expirationDate; + + expect(isExpired()).toBe(false); + + // Advance 4 days + rstest.advanceTimersByTime(4 * 24 * 60 * 60 * 1000); + expect(isExpired()).toBe(false); + + // Advance 2 more days (total 6 days = Jan 21) + rstest.advanceTimersByTime(2 * 24 * 60 * 60 * 1000); + expect(isExpired()).toBe(true); + }); + + it('should test scheduled task logic', () => { + rstest.setSystemTime(new Date('2024-01-01T08:00:00.000Z')); + + const scheduledTasks: string[] = []; + + // Task scheduled for noon + const noonTime = new Date('2024-01-01T12:00:00.000Z').getTime(); + const checkTime = () => { + if (Date.now() >= noonTime) { + scheduledTasks.push('noon-task'); + } + }; + + checkTime(); + expect(scheduledTasks).toEqual([]); + + // Advance to 11:00 + rstest.advanceTimersByTime(3 * 60 * 60 * 1000); + checkTime(); + expect(scheduledTasks).toEqual([]); + + // Advance to 12:30 + rstest.advanceTimersByTime(1.5 * 60 * 60 * 1000); + checkTime(); + expect(scheduledTasks).toEqual(['noon-task']); + }); +}); diff --git a/rstest/fake-timers/tests/timing.test.ts b/rstest/fake-timers/tests/timing.test.ts new file mode 100644 index 00000000..3dc0f4a4 --- /dev/null +++ b/rstest/fake-timers/tests/timing.test.ts @@ -0,0 +1,181 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; +import { debounce, delay, throttle } from '../src/timing'; + +describe('Testing debounce with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should not call function before delay', () => { + const fn = rstest.fn(); + const debounced = debounce(fn, 500); + + debounced(); + + expect(fn).not.toHaveBeenCalled(); + }); + + it('should call function after delay', () => { + const fn = rstest.fn(); + const debounced = debounce(fn, 500); + + debounced(); + + rstest.advanceTimersByTime(500); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should reset timer on subsequent calls', () => { + const fn = rstest.fn(); + const debounced = debounce(fn, 500); + + debounced(); + rstest.advanceTimersByTime(300); + + debounced(); + rstest.advanceTimersByTime(300); + + expect(fn).not.toHaveBeenCalled(); + + rstest.advanceTimersByTime(200); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should pass arguments to debounced function', () => { + const fn = rstest.fn(); + const debounced = debounce(fn, 500); + + debounced('arg1', 'arg2'); + + rstest.advanceTimersByTime(500); + + expect(fn).toHaveBeenCalledWith('arg1', 'arg2'); + }); + + it('should use last arguments when called multiple times', () => { + const fn = rstest.fn(); + const debounced = debounce(fn, 500); + + debounced('first'); + debounced('second'); + debounced('third'); + + rstest.advanceTimersByTime(500); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith('third'); + }); +}); + +describe('Testing throttle with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should call function immediately on first call', () => { + const fn = rstest.fn(); + const throttled = throttle(fn, 500); + + throttled(); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should not call function again within wait period', () => { + const fn = rstest.fn(); + const throttled = throttle(fn, 500); + + throttled(); + throttled(); + throttled(); + + expect(fn).toHaveBeenCalledTimes(1); + }); + + it('should call function again after wait period', () => { + const fn = rstest.fn(); + const throttled = throttle(fn, 500); + + throttled(); + expect(fn).toHaveBeenCalledTimes(1); + + rstest.advanceTimersByTime(500); + + throttled(); + expect(fn).toHaveBeenCalledTimes(2); + }); + + it('should queue trailing call', () => { + const fn = rstest.fn(); + const throttled = throttle(fn, 500); + + throttled('first'); + throttled('second'); + + expect(fn).toHaveBeenCalledTimes(1); + expect(fn).toHaveBeenCalledWith('first'); + + rstest.advanceTimersByTime(500); + + expect(fn).toHaveBeenCalledTimes(2); + expect(fn).toHaveBeenLastCalledWith('second'); + }); +}); + +describe('Testing delay with Fake Timers', () => { + beforeEach(() => { + rstest.useFakeTimers(); + }); + + afterEach(() => { + rstest.useRealTimers(); + }); + + it('should resolve after specified delay', async () => { + let resolved = false; + + delay(1000).then(() => { + resolved = true; + }); + + expect(resolved).toBe(false); + + await rstest.advanceTimersByTimeAsync(1000); + + expect(resolved).toBe(true); + }); + + it('should work with async/await', async () => { + const sequence: string[] = []; + + const asyncFn = async () => { + sequence.push('start'); + await delay(500); + sequence.push('middle'); + await delay(500); + sequence.push('end'); + }; + + const promise = asyncFn(); + + expect(sequence).toEqual(['start']); + + await rstest.advanceTimersByTimeAsync(500); + expect(sequence).toEqual(['start', 'middle']); + + await rstest.advanceTimersByTimeAsync(500); + expect(sequence).toEqual(['start', 'middle', 'end']); + + await promise; + }); +}); diff --git a/rstest/fake-timers/tsconfig.json b/rstest/fake-timers/tsconfig.json new file mode 100644 index 00000000..5d1c2508 --- /dev/null +++ b/rstest/fake-timers/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts"] +} diff --git a/rstest/mocking/package.json b/rstest/mocking/package.json new file mode 100644 index 00000000..4ae75db2 --- /dev/null +++ b/rstest/mocking/package.json @@ -0,0 +1,12 @@ +{ + "name": "rstest-mocking", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "rstest run" + }, + "devDependencies": { + "@rstest/core": "^0.7.9", + "typescript": "^5.8.3" + } +} diff --git a/rstest/mocking/rstest.config.ts b/rstest/mocking/rstest.config.ts new file mode 100644 index 00000000..9ee3cbab --- /dev/null +++ b/rstest/mocking/rstest.config.ts @@ -0,0 +1,3 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({}); diff --git a/rstest/mocking/src/api.ts b/rstest/mocking/src/api.ts new file mode 100644 index 00000000..3075eccf --- /dev/null +++ b/rstest/mocking/src/api.ts @@ -0,0 +1,55 @@ +/** + * API client for mocking demonstration + */ + +export interface User { + id: number; + name: string; + email: string; +} + +export interface Post { + id: number; + title: string; + body: string; + userId: number; +} + +const API_BASE = 'https://api.example.com'; + +/** + * Fetch user by ID + */ +export async function fetchUser(id: number): Promise { + const response = await fetch(`${API_BASE}/users/${id}`); + if (!response.ok) { + throw new Error(`Failed to fetch user: ${response.status}`); + } + return response.json(); +} + +/** + * Fetch posts by user ID + */ +export async function fetchUserPosts(userId: number): Promise { + const response = await fetch(`${API_BASE}/users/${userId}/posts`); + if (!response.ok) { + throw new Error(`Failed to fetch posts: ${response.status}`); + } + return response.json(); +} + +/** + * Create a new post + */ +export async function createPost(post: Omit): Promise { + const response = await fetch(`${API_BASE}/posts`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(post), + }); + if (!response.ok) { + throw new Error(`Failed to create post: ${response.status}`); + } + return response.json(); +} diff --git a/rstest/mocking/src/index.ts b/rstest/mocking/src/index.ts new file mode 100644 index 00000000..350dad1c --- /dev/null +++ b/rstest/mocking/src/index.ts @@ -0,0 +1,4 @@ +export * from './api'; +export * from './logger'; +export * from './user-service'; +export * from './utils'; diff --git a/rstest/mocking/src/logger.ts b/rstest/mocking/src/logger.ts new file mode 100644 index 00000000..7badc642 --- /dev/null +++ b/rstest/mocking/src/logger.ts @@ -0,0 +1,67 @@ +/** + * Logger service for mocking demonstration + */ + +export type LogLevel = 'debug' | 'info' | 'warn' | 'error'; + +export interface LogEntry { + level: LogLevel; + message: string; + timestamp: Date; + metadata?: Record; +} + +export class Logger { + private name: string; + private entries: LogEntry[] = []; + + constructor(name: string) { + this.name = name; + } + + private createEntry( + level: LogLevel, + message: string, + metadata?: Record, + ): LogEntry { + const entry: LogEntry = { + level, + message: `[${this.name}] ${message}`, + timestamp: new Date(), + metadata, + }; + this.entries.push(entry); + return entry; + } + + debug(message: string, metadata?: Record): void { + const entry = this.createEntry('debug', message, metadata); + console.debug(entry.message, metadata); + } + + info(message: string, metadata?: Record): void { + const entry = this.createEntry('info', message, metadata); + console.info(entry.message, metadata); + } + + warn(message: string, metadata?: Record): void { + const entry = this.createEntry('warn', message, metadata); + console.warn(entry.message, metadata); + } + + error(message: string, metadata?: Record): void { + const entry = this.createEntry('error', message, metadata); + console.error(entry.message, metadata); + } + + getEntries(): LogEntry[] { + return [...this.entries]; + } + + clear(): void { + this.entries = []; + } +} + +// Default logger instance +export const logger = new Logger('App'); diff --git a/rstest/mocking/src/user-service.ts b/rstest/mocking/src/user-service.ts new file mode 100644 index 00000000..43a6f71a --- /dev/null +++ b/rstest/mocking/src/user-service.ts @@ -0,0 +1,35 @@ +import { fetchUser, fetchUserPosts, type Post, type User } from './api'; +import { logger } from './logger'; + +/** + * User service that uses API and logger + */ +export class UserService { + async getUser(id: number): Promise { + logger.info(`Fetching user with ID: ${id}`); + try { + const user = await fetchUser(id); + logger.info(`Successfully fetched user: ${user.name}`); + return user; + } catch (error) { + logger.error(`Failed to fetch user: ${error}`); + throw error; + } + } + + async getUserWithPosts(id: number): Promise<{ user: User; posts: Post[] }> { + logger.info(`Fetching user and posts for ID: ${id}`); + + const user = await fetchUser(id); + const posts = await fetchUserPosts(id); + + logger.info(`Fetched ${posts.length} posts for user: ${user.name}`); + + return { user, posts }; + } + + async getUserNames(ids: number[]): Promise { + const users = await Promise.all(ids.map((id) => fetchUser(id))); + return users.map((user) => user.name); + } +} diff --git a/rstest/mocking/src/utils.ts b/rstest/mocking/src/utils.ts new file mode 100644 index 00000000..c7df1558 --- /dev/null +++ b/rstest/mocking/src/utils.ts @@ -0,0 +1,38 @@ +/** + * Utility functions for mocking demonstration + */ + +/** + * Generate a random ID + */ +export function generateId(): string { + return Math.random().toString(36).substring(2, 15); +} + +/** + * Get current timestamp + */ +export function getTimestamp(): number { + return Date.now(); +} + +/** + * Format a date + */ +export function formatDate(date: Date): string { + return date.toISOString(); +} + +/** + * Sleep for a given number of milliseconds + */ +export function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** + * Log a message with timestamp + */ +export function log(message: string): void { + console.log(`[${new Date().toISOString()}] ${message}`); +} diff --git a/rstest/mocking/tests/dynamic-mock.test.ts b/rstest/mocking/tests/dynamic-mock.test.ts new file mode 100644 index 00000000..c7d57e21 --- /dev/null +++ b/rstest/mocking/tests/dynamic-mock.test.ts @@ -0,0 +1,158 @@ +import { afterEach, beforeEach, describe, expect, it, rs, rstest } from '@rstest/core'; + +describe('rs.doMock() - Dynamic Module Mocking', () => { + /** + * rs.doMock() is NOT hoisted, allowing you to mock modules dynamically + * based on test conditions. Use with dynamic imports. + */ + + beforeEach(() => { + rs.resetModules(); + }); + + afterEach(() => { + rs.resetModules(); + }); + + it('should dynamically mock a module', async () => { + rs.doMock('../src/utils', () => ({ + generateId: () => 'fixed-id-123', + getTimestamp: () => 1000000, + })); + + const { generateId, getTimestamp } = await import('../src/utils'); + + expect(generateId()).toBe('fixed-id-123'); + expect(getTimestamp()).toBe(1000000); + }); + + it('should use different mocks in different tests', async () => { + rs.doMock('../src/utils', () => ({ + generateId: () => 'different-id', + getTimestamp: () => 2000000, + })); + + const { generateId, getTimestamp } = await import('../src/utils'); + + expect(generateId()).toBe('different-id'); + expect(getTimestamp()).toBe(2000000); + }); + + it('should conditionally mock based on test conditions', async () => { + const shouldMock = true; + + if (shouldMock) { + rs.doMock('../src/utils', () => ({ + generateId: () => 'conditional-mock', + getTimestamp: () => 0, + })); + } + + const { generateId } = await import('../src/utils'); + + expect(generateId()).toBe('conditional-mock'); + }); +}); + +describe('rs.doUnmock() - Remove Dynamic Mocks', () => { + beforeEach(() => { + rs.resetModules(); + }); + + afterEach(() => { + rs.resetModules(); + }); + + it('should remove dynamic mock', async () => { + rs.doMock('../src/utils', () => ({ + formatDate: () => 'mocked-date', + })); + + const mocked = await import('../src/utils'); + expect(mocked.formatDate(new Date())).toBe('mocked-date'); + + rs.doUnmock('../src/utils'); + rs.resetModules(); + + const original = await import('../src/utils'); + // Now uses original implementation + expect(original.formatDate(new Date('2024-01-01'))).toBe('2024-01-01T00:00:00.000Z'); + }); +}); + +describe('rs.importActual() - Import Original Module', () => { + /** + * When you need to use the original module implementation + * alongside mocked parts + */ + + it('should import actual module inside mock', async () => { + rs.doMock('../src/utils', async () => { + const actual = await rs.importActual('../src/utils'); + + return { + ...actual, + // Override only specific functions + generateId: () => 'overridden-id', + }; + }); + + const utils = await import('../src/utils'); + + // Overridden function + expect(utils.generateId()).toBe('overridden-id'); + + // Original functions still work + expect(utils.formatDate(new Date('2024-01-01'))).toBe('2024-01-01T00:00:00.000Z'); + }); +}); + +describe('rs.importMock() - Import Mocked Module', () => { + /** + * rs.importMock() returns the mocked version of a module + * Note: This requires the module to be mocked first + */ + + beforeEach(() => { + rs.resetModules(); + }); + + it('should get mocked module via dynamic import', async () => { + const mockFn = rstest.fn().mockReturnValue('mock-id'); + + rs.doMock('../src/utils', () => ({ + generateId: mockFn, + })); + + // Use dynamic import to get the mocked module + const mockedUtils = await import('../src/utils'); + + expect(mockedUtils.generateId()).toBe('mock-id'); + expect(mockFn).toHaveBeenCalled(); + }); +}); + +describe('rs.resetModules() - Reset Module Registry', () => { + /** + * rs.resetModules() clears the module cache, + * allowing fresh imports + */ + + it('should reset module cache', async () => { + rs.doMock('../src/utils', () => ({ + generateId: () => 'first-mock', + })); + + const first = await import('../src/utils'); + expect(first.generateId()).toBe('first-mock'); + + rs.resetModules(); + + rs.doMock('../src/utils', () => ({ + generateId: () => 'second-mock', + })); + + const second = await import('../src/utils'); + expect(second.generateId()).toBe('second-mock'); + }); +}); diff --git a/rstest/mocking/tests/fn.test.ts b/rstest/mocking/tests/fn.test.ts new file mode 100644 index 00000000..1f502523 --- /dev/null +++ b/rstest/mocking/tests/fn.test.ts @@ -0,0 +1,231 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; + +describe('rstest.fn() - Mock Functions', () => { + /** + * rstest.fn() creates a mock function that tracks calls, + * arguments, and return values + */ + + describe('Basic Usage', () => { + it('should create a mock function', () => { + const mockFn = rstest.fn(); + + mockFn(); + mockFn('arg1', 'arg2'); + + expect(mockFn).toHaveBeenCalled(); + expect(mockFn).toHaveBeenCalledTimes(2); + }); + + it('should track call arguments', () => { + const mockFn = rstest.fn(); + + mockFn('first'); + mockFn('second', 123); + mockFn({ key: 'value' }); + + expect(mockFn).toHaveBeenCalledWith('first'); + expect(mockFn).toHaveBeenCalledWith('second', 123); + expect(mockFn).toHaveBeenCalledWith({ key: 'value' }); + expect(mockFn).toHaveBeenLastCalledWith({ key: 'value' }); + expect(mockFn).toHaveBeenNthCalledWith(1, 'first'); + }); + }); + + describe('mockReturnValue', () => { + it('should return specified value', () => { + const mockFn = rstest.fn().mockReturnValue(42); + + expect(mockFn()).toBe(42); + expect(mockFn()).toBe(42); + }); + + it('should return different values with mockReturnValueOnce', () => { + const mockFn = rstest.fn().mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(0); + + expect(mockFn()).toBe(1); + expect(mockFn()).toBe(2); + expect(mockFn()).toBe(0); + expect(mockFn()).toBe(0); + }); + }); + + describe('mockImplementation', () => { + it('should use custom implementation', () => { + const mockFn = rstest.fn().mockImplementation((a: number, b: number) => a + b); + + expect(mockFn(2, 3)).toBe(5); + expect(mockFn(10, 20)).toBe(30); + }); + + it('should use mockImplementationOnce', () => { + const mockFn = rstest + .fn() + .mockImplementationOnce(() => 'first') + .mockImplementationOnce(() => 'second') + .mockImplementation(() => 'default'); + + expect(mockFn()).toBe('first'); + expect(mockFn()).toBe('second'); + expect(mockFn()).toBe('default'); + }); + }); + + describe('mockResolvedValue / mockRejectedValue', () => { + it('should return resolved promise', async () => { + const mockFn = rstest.fn().mockResolvedValue('success'); + + const result = await mockFn(); + expect(result).toBe('success'); + }); + + it('should return resolved values sequentially', async () => { + const mockFn = rstest + .fn() + .mockResolvedValueOnce('first') + .mockResolvedValueOnce('second') + .mockResolvedValue('default'); + + expect(await mockFn()).toBe('first'); + expect(await mockFn()).toBe('second'); + expect(await mockFn()).toBe('default'); + }); + + it('should return rejected promise', async () => { + const mockFn = rstest.fn().mockRejectedValue(new Error('failed')); + + await expect(mockFn()).rejects.toThrow('failed'); + }); + + it('should reject once then resolve', async () => { + const mockFn = rstest + .fn() + .mockRejectedValueOnce(new Error('first attempt failed')) + .mockResolvedValue('success'); + + await expect(mockFn()).rejects.toThrow('first attempt failed'); + expect(await mockFn()).toBe('success'); + }); + }); + + describe('Mock Properties', () => { + it('should access mock.calls', () => { + const mockFn = rstest.fn(); + + mockFn('a', 1); + mockFn('b', 2); + + expect(mockFn.mock.calls).toEqual([ + ['a', 1], + ['b', 2], + ]); + }); + + it('should access mock.results', () => { + const mockFn = rstest.fn().mockReturnValueOnce(10).mockReturnValueOnce(20); + + mockFn(); + mockFn(); + + expect(mockFn.mock.results).toEqual([ + { type: 'return', value: 10 }, + { type: 'return', value: 20 }, + ]); + }); + + it('should access mock.lastCall', () => { + const mockFn = rstest.fn(); + + mockFn('first'); + mockFn('last'); + + expect(mockFn.mock.lastCall).toEqual(['last']); + }); + }); + + describe('Reset and Clear', () => { + it('should clear mock with mockClear', () => { + const mockFn = rstest.fn().mockReturnValue(42); + + mockFn(); + mockFn(); + + mockFn.mockClear(); + + expect(mockFn.mock.calls).toEqual([]); + expect(mockFn()).toBe(42); // Implementation still works + }); + + it('should reset mock with mockReset', () => { + const mockFn = rstest.fn().mockReturnValue(42); + + mockFn(); + + mockFn.mockReset(); + + expect(mockFn.mock.calls).toEqual([]); + expect(mockFn()).toBeUndefined(); // Implementation is reset + }); + + it('should restore mock with mockRestore', () => { + const mockFn = rstest.fn().mockReturnValue(42); + + mockFn(); + + mockFn.mockRestore(); + + expect(mockFn.mock.calls).toEqual([]); + }); + }); +}); + +describe('rstest.isMockFunction()', () => { + it('should return true for mock functions', () => { + const mockFn = rstest.fn(); + expect(rstest.isMockFunction(mockFn)).toBe(true); + }); + + it('should return false for regular functions', () => { + const regularFn = () => {}; + expect(rstest.isMockFunction(regularFn)).toBe(false); + }); +}); + +describe('Global Mock Management', () => { + let mock1: ReturnType; + let mock2: ReturnType; + + beforeEach(() => { + mock1 = rstest.fn().mockReturnValue('a'); + mock2 = rstest.fn().mockReturnValue('b'); + + mock1(); + mock2(); + }); + + afterEach(() => { + rstest.restoreAllMocks(); + }); + + it('should clear all mocks with rstest.clearAllMocks', () => { + rstest.clearAllMocks(); + + expect(mock1.mock.calls).toEqual([]); + expect(mock2.mock.calls).toEqual([]); + + // Implementations still work + expect(mock1()).toBe('a'); + expect(mock2()).toBe('b'); + }); + + it('should reset all mocks with rstest.resetAllMocks', () => { + rstest.resetAllMocks(); + + expect(mock1.mock.calls).toEqual([]); + expect(mock2.mock.calls).toEqual([]); + + // Implementations are reset + expect(mock1()).toBeUndefined(); + expect(mock2()).toBeUndefined(); + }); +}); diff --git a/rstest/mocking/tests/matchers.test.ts b/rstest/mocking/tests/matchers.test.ts new file mode 100644 index 00000000..355efa34 --- /dev/null +++ b/rstest/mocking/tests/matchers.test.ts @@ -0,0 +1,197 @@ +import { describe, expect, it, rstest } from '@rstest/core'; + +describe('Mock Matchers', () => { + /** + * Special matchers for asserting mock function calls + */ + + describe('toHaveBeenCalled / toHaveBeenCalledTimes', () => { + it('should check if mock was called', () => { + const mockFn = rstest.fn(); + + expect(mockFn).not.toHaveBeenCalled(); + + mockFn(); + + expect(mockFn).toHaveBeenCalled(); + }); + + it('should check exact call count', () => { + const mockFn = rstest.fn(); + + mockFn(); + mockFn(); + mockFn(); + + expect(mockFn).toHaveBeenCalledTimes(3); + }); + }); + + describe('toHaveBeenCalledWith', () => { + it('should check call arguments', () => { + const mockFn = rstest.fn(); + + mockFn('hello', 123, { key: 'value' }); + + expect(mockFn).toHaveBeenCalledWith('hello', 123, { key: 'value' }); + }); + + it('should work with partial matchers', () => { + const mockFn = rstest.fn(); + + mockFn({ name: 'John', age: 30, city: 'NYC' }); + + expect(mockFn).toHaveBeenCalledWith(expect.objectContaining({ name: 'John' })); + }); + + it('should work with array matchers', () => { + const mockFn = rstest.fn(); + + mockFn([1, 2, 3, 4, 5]); + + expect(mockFn).toHaveBeenCalledWith(expect.arrayContaining([1, 2, 3])); + }); + }); + + describe('toHaveBeenLastCalledWith', () => { + it('should check last call arguments', () => { + const mockFn = rstest.fn(); + + mockFn('first'); + mockFn('second'); + mockFn('last'); + + expect(mockFn).toHaveBeenLastCalledWith('last'); + }); + }); + + describe('toHaveBeenNthCalledWith', () => { + it('should check nth call arguments', () => { + const mockFn = rstest.fn(); + + mockFn('a'); + mockFn('b'); + mockFn('c'); + + expect(mockFn).toHaveBeenNthCalledWith(1, 'a'); + expect(mockFn).toHaveBeenNthCalledWith(2, 'b'); + expect(mockFn).toHaveBeenNthCalledWith(3, 'c'); + }); + }); + + describe('toHaveReturned / toHaveReturnedWith', () => { + it('should check if mock returned', () => { + const mockFn = rstest.fn().mockReturnValue(42); + + mockFn(); + + expect(mockFn).toHaveReturned(); + }); + + it('should check return value', () => { + const mockFn = rstest.fn().mockReturnValue('result'); + + mockFn(); + + expect(mockFn).toHaveReturnedWith('result'); + }); + + it('should check return times', () => { + const mockFn = rstest.fn().mockReturnValue(true); + + mockFn(); + mockFn(); + + expect(mockFn).toHaveReturnedTimes(2); + }); + + it('should check last return value', () => { + const mockFn = rstest + .fn() + .mockReturnValueOnce(1) + .mockReturnValueOnce(2) + .mockReturnValueOnce(3); + + mockFn(); + mockFn(); + mockFn(); + + expect(mockFn).toHaveLastReturnedWith(3); + }); + + it('should check nth return value', () => { + const mockFn = rstest.fn().mockReturnValueOnce('first').mockReturnValueOnce('second'); + + mockFn(); + mockFn(); + + expect(mockFn).toHaveNthReturnedWith(1, 'first'); + expect(mockFn).toHaveNthReturnedWith(2, 'second'); + }); + }); +}); + +describe('Asymmetric Matchers with Mocks', () => { + it('should use expect.any()', () => { + const mockFn = rstest.fn(); + + mockFn(42, 'string', new Date()); + + expect(mockFn).toHaveBeenCalledWith(expect.any(Number), expect.any(String), expect.any(Date)); + }); + + it('should use expect.stringContaining()', () => { + const mockFn = rstest.fn(); + + mockFn('Hello, World!'); + + expect(mockFn).toHaveBeenCalledWith(expect.stringContaining('World')); + }); + + it('should use expect.stringMatching()', () => { + const mockFn = rstest.fn(); + + mockFn('user@example.com'); + + expect(mockFn).toHaveBeenCalledWith(expect.stringMatching(/@example\.com$/)); + }); + + it('should use expect.objectContaining()', () => { + const mockFn = rstest.fn(); + + mockFn({ + id: 1, + name: 'Test', + email: 'test@example.com', + createdAt: new Date(), + }); + + expect(mockFn).toHaveBeenCalledWith( + expect.objectContaining({ + id: 1, + name: 'Test', + }), + ); + }); + + it('should combine multiple asymmetric matchers', () => { + const mockFn = rstest.fn(); + + mockFn({ + user: { id: 123, name: 'John' }, + timestamp: Date.now(), + message: 'User logged in successfully', + }); + + expect(mockFn).toHaveBeenCalledWith( + expect.objectContaining({ + user: expect.objectContaining({ + id: expect.any(Number), + name: expect.any(String), + }), + timestamp: expect.any(Number), + message: expect.stringContaining('logged in'), + }), + ); + }); +}); diff --git a/rstest/mocking/tests/module-mock.test.ts b/rstest/mocking/tests/module-mock.test.ts new file mode 100644 index 00000000..5c945106 --- /dev/null +++ b/rstest/mocking/tests/module-mock.test.ts @@ -0,0 +1,91 @@ +import { afterEach, beforeEach, describe, expect, it, rs } from '@rstest/core'; + +/** + * rs.mock() is hoisted to the top of the file and replaces the module. + * Use rs.hoisted() to define mock values that need to be available during hoisting. + */ + +// Using rs.hoisted to define mock values before rs.mock is hoisted +const { mockFetchUser, mockFetchUserPosts } = rs.hoisted(() => { + return { + mockFetchUser: rstest.fn(), + mockFetchUserPosts: rstest.fn(), + }; +}); + +// Mock the API module +rs.mock('../src/api', () => ({ + fetchUser: mockFetchUser, + fetchUserPosts: mockFetchUserPosts, + createPost: rstest.fn(), +})); + +import { rstest } from '@rstest/core'; +// Import after mocking - the import will use the mocked version +import { UserService } from '../src/user-service'; + +describe('rs.mock() - Module Mocking', () => { + let userService: UserService; + + beforeEach(() => { + userService = new UserService(); + rstest.clearAllMocks(); + }); + + afterEach(() => { + rstest.resetAllMocks(); + }); + + describe('Mocking API Calls', () => { + it('should use mocked fetchUser', async () => { + mockFetchUser.mockResolvedValue({ + id: 1, + name: 'Mock User', + email: 'mock@example.com', + }); + + const user = await userService.getUser(1); + + expect(user.name).toBe('Mock User'); + expect(mockFetchUser).toHaveBeenCalledWith(1); + }); + + it('should use mocked fetchUserPosts', async () => { + mockFetchUser.mockResolvedValue({ + id: 1, + name: 'Test User', + email: 'test@example.com', + }); + + mockFetchUserPosts.mockResolvedValue([ + { id: 1, title: 'Post 1', body: 'Body 1', userId: 1 }, + { id: 2, title: 'Post 2', body: 'Body 2', userId: 1 }, + ]); + + const { user, posts } = await userService.getUserWithPosts(1); + + expect(user.name).toBe('Test User'); + expect(posts.length).toBe(2); + expect(mockFetchUser).toHaveBeenCalledWith(1); + expect(mockFetchUserPosts).toHaveBeenCalledWith(1); + }); + + it('should handle errors from mocked API', async () => { + mockFetchUser.mockRejectedValue(new Error('API Error')); + + await expect(userService.getUser(1)).rejects.toThrow('API Error'); + }); + + it('should mock multiple user fetches', async () => { + mockFetchUser + .mockResolvedValueOnce({ id: 1, name: 'User 1', email: 'user1@example.com' }) + .mockResolvedValueOnce({ id: 2, name: 'User 2', email: 'user2@example.com' }) + .mockResolvedValueOnce({ id: 3, name: 'User 3', email: 'user3@example.com' }); + + const names = await userService.getUserNames([1, 2, 3]); + + expect(names).toEqual(['User 1', 'User 2', 'User 3']); + expect(mockFetchUser).toHaveBeenCalledTimes(3); + }); + }); +}); diff --git a/rstest/mocking/tests/spy.test.ts b/rstest/mocking/tests/spy.test.ts new file mode 100644 index 00000000..62b0d161 --- /dev/null +++ b/rstest/mocking/tests/spy.test.ts @@ -0,0 +1,176 @@ +import { afterEach, beforeEach, describe, expect, it, rstest } from '@rstest/core'; +import { Logger } from '../src/logger'; + +describe('rstest.spyOn() - Spying on Methods', () => { + /** + * rstest.spyOn() creates a spy on an existing method, + * allowing you to track calls while preserving the original implementation + */ + + describe('Basic Usage', () => { + it('should spy on object method', () => { + const calculator = { + add: (a: number, b: number) => a + b, + }; + + const spy = rstest.spyOn(calculator, 'add'); + + const result = calculator.add(2, 3); + + expect(result).toBe(5); + expect(spy).toHaveBeenCalled(); + expect(spy).toHaveBeenCalledWith(2, 3); + }); + + it('should track multiple calls', () => { + const obj = { + greet: (name: string) => `Hello, ${name}!`, + }; + + const spy = rstest.spyOn(obj, 'greet'); + + obj.greet('Alice'); + obj.greet('Bob'); + obj.greet('Charlie'); + + expect(spy).toHaveBeenCalledTimes(3); + expect(spy).toHaveBeenNthCalledWith(1, 'Alice'); + expect(spy).toHaveBeenNthCalledWith(2, 'Bob'); + expect(spy).toHaveBeenNthCalledWith(3, 'Charlie'); + }); + }); + + describe('Mocking Implementation', () => { + it('should mock the implementation', () => { + const api = { + fetchData: () => 'real data', + }; + + const spy = rstest.spyOn(api, 'fetchData').mockReturnValue('mocked data'); + + expect(api.fetchData()).toBe('mocked data'); + expect(spy).toHaveBeenCalled(); + }); + + it('should mock with custom implementation', () => { + const math = { + multiply: (a: number, b: number) => a * b, + }; + + rstest.spyOn(math, 'multiply').mockImplementation((a, b) => a + b); + + expect(math.multiply(2, 3)).toBe(5); // Returns sum instead of product + }); + + it('should restore original implementation', () => { + const obj = { + getValue: () => 'original', + }; + + const spy = rstest.spyOn(obj, 'getValue').mockReturnValue('mocked'); + + expect(obj.getValue()).toBe('mocked'); + + spy.mockRestore(); + + expect(obj.getValue()).toBe('original'); + }); + }); + + describe('Spying on Class Methods', () => { + let logger: Logger; + + beforeEach(() => { + logger = new Logger('Test'); + }); + + afterEach(() => { + rstest.restoreAllMocks(); + }); + + it('should spy on class instance method', () => { + const infoSpy = rstest.spyOn(logger, 'info'); + + logger.info('Test message'); + + expect(infoSpy).toHaveBeenCalledWith('Test message'); + }); + + it('should spy on multiple methods', () => { + const infoSpy = rstest.spyOn(logger, 'info'); + const errorSpy = rstest.spyOn(logger, 'error'); + + logger.info('Info message'); + logger.error('Error message'); + + expect(infoSpy).toHaveBeenCalledWith('Info message'); + expect(errorSpy).toHaveBeenCalledWith('Error message'); + }); + + it('should mock class method implementation', () => { + const warnSpy = rstest.spyOn(logger, 'warn').mockImplementation(() => { + // Do nothing - suppress output + }); + + logger.warn('This should be suppressed'); + + expect(warnSpy).toHaveBeenCalled(); + }); + }); + + describe('Spying on Console', () => { + afterEach(() => { + rstest.restoreAllMocks(); + }); + + it('should spy on console.log', () => { + const consoleSpy = rstest.spyOn(console, 'log').mockImplementation(() => {}); + + console.log('Hello', 'World'); + + expect(consoleSpy).toHaveBeenCalledWith('Hello', 'World'); + }); + + it('should spy on console.error', () => { + const consoleSpy = rstest.spyOn(console, 'error').mockImplementation(() => {}); + + console.error('Error occurred'); + + expect(consoleSpy).toHaveBeenCalledWith('Error occurred'); + }); + }); + + describe('Spying on Getters and Setters', () => { + it('should spy on getter', () => { + const obj = { + _value: 42, + get value() { + return this._value; + }, + }; + + const spy = rstest.spyOn(obj, 'value', 'get').mockReturnValue(100); + + expect(obj.value).toBe(100); + expect(spy).toHaveBeenCalled(); + }); + + it('should spy on setter', () => { + const obj = { + _value: 0, + get value() { + return this._value; + }, + set value(v: number) { + this._value = v; + }, + }; + + const spy = rstest.spyOn(obj, 'value', 'set'); + + obj.value = 42; + + expect(spy).toHaveBeenCalledWith(42); + }); + }); +}); diff --git a/rstest/mocking/tsconfig.json b/rstest/mocking/tsconfig.json new file mode 100644 index 00000000..5d1c2508 --- /dev/null +++ b/rstest/mocking/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts"] +} diff --git a/rstest/rsbuild-adapter/package.json b/rstest/rsbuild-adapter/package.json new file mode 100644 index 00000000..fefef090 --- /dev/null +++ b/rstest/rsbuild-adapter/package.json @@ -0,0 +1,17 @@ +{ + "name": "rstest-rsbuild-adapter", + "version": "1.0.0", + "private": true, + "scripts": { + "dev": "rsbuild dev", + "build": "rsbuild build", + "test": "rstest run" + }, + "devDependencies": { + "@rsbuild/core": "^1.7.2", + "@rstest/adapter-rsbuild": "^0.1.0", + "@rstest/core": "^0.7.9", + "happy-dom": "^18.0.1", + "typescript": "^5.8.3" + } +} diff --git a/rstest/rsbuild-adapter/rsbuild.config.ts b/rstest/rsbuild-adapter/rsbuild.config.ts new file mode 100644 index 00000000..9f8ca5b6 --- /dev/null +++ b/rstest/rsbuild-adapter/rsbuild.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from '@rsbuild/core'; + +export default defineConfig({ + source: { + entry: { + index: './src/index.ts', + }, + define: { + __APP_VERSION__: JSON.stringify('2.0.0'), + __PLATFORM__: JSON.stringify('web'), + 'process.env.APP_NAME': JSON.stringify('my-app'), + }, + alias: { + '@': './src', + '@utils': './src/utils', + }, + }, +}); diff --git a/rstest/rsbuild-adapter/rstest.config.ts b/rstest/rsbuild-adapter/rstest.config.ts new file mode 100644 index 00000000..33007c16 --- /dev/null +++ b/rstest/rsbuild-adapter/rstest.config.ts @@ -0,0 +1,9 @@ +import { withRsbuildConfig } from '@rstest/adapter-rsbuild'; +import { defineConfig, type ExtendConfigFn } from '@rstest/core'; + +export default defineConfig({ + // Type assertion needed due to adapter package version mismatch + extends: withRsbuildConfig({ + environmentName: 'web', + }) as ExtendConfigFn, +}); diff --git a/rstest/rsbuild-adapter/src/index.ts b/rstest/rsbuild-adapter/src/index.ts new file mode 100644 index 00000000..298506d5 --- /dev/null +++ b/rstest/rsbuild-adapter/src/index.ts @@ -0,0 +1,22 @@ +import { capitalize } from '@utils/string'; + +declare const __APP_VERSION__: string; +declare const __PLATFORM__: string; + +export function getAppVersion(): string { + return __APP_VERSION__; +} + +export function getPlatform(): string { + return __PLATFORM__; +} + +export function getAppName(): string { + return process.env.APP_NAME || 'unknown'; +} + +export function formatAppInfo(): string { + return `${capitalize(getAppName())} v${getAppVersion()} (${getPlatform()})`; +} + +export { capitalize } from '@utils/string'; diff --git a/rstest/rsbuild-adapter/src/utils/index.ts b/rstest/rsbuild-adapter/src/utils/index.ts new file mode 100644 index 00000000..57f9f48d --- /dev/null +++ b/rstest/rsbuild-adapter/src/utils/index.ts @@ -0,0 +1 @@ +export * from './string'; diff --git a/rstest/rsbuild-adapter/src/utils/string.ts b/rstest/rsbuild-adapter/src/utils/string.ts new file mode 100644 index 00000000..e9619c59 --- /dev/null +++ b/rstest/rsbuild-adapter/src/utils/string.ts @@ -0,0 +1,8 @@ +export function capitalize(str: string): string { + if (!str) return ''; + return str.charAt(0).toUpperCase() + str.slice(1); +} + +export function lowercase(str: string): string { + return str.toLowerCase(); +} diff --git a/rstest/rsbuild-adapter/tests/index.test.ts b/rstest/rsbuild-adapter/tests/index.test.ts new file mode 100644 index 00000000..fb0242c9 --- /dev/null +++ b/rstest/rsbuild-adapter/tests/index.test.ts @@ -0,0 +1,28 @@ +import { describe, expect, it } from '@rstest/core'; +import { formatAppInfo, getAppName, getAppVersion, getPlatform } from '@/index'; + +describe('withRsbuildConfig - define inheritance', () => { + it('should inherit __APP_VERSION__ from rsbuild.config.ts', () => { + expect(getAppVersion()).toBe('2.0.0'); + }); + + it('should inherit __PLATFORM__ from rsbuild.config.ts', () => { + expect(getPlatform()).toBe('web'); + }); + + it('should inherit process.env.APP_NAME from rsbuild.config.ts', () => { + expect(getAppName()).toBe('my-app'); + }); +}); + +describe('withRsbuildConfig - alias inheritance', () => { + it('should resolve @/ alias to src directory', () => { + expect(typeof formatAppInfo).toBe('function'); + expect(formatAppInfo()).toBe('My-app v2.0.0 (web)'); + }); + + it('should resolve @utils alias', async () => { + const { capitalize } = await import('@utils/string'); + expect(capitalize('hello')).toBe('Hello'); + }); +}); diff --git a/rstest/rsbuild-adapter/tsconfig.json b/rstest/rsbuild-adapter/tsconfig.json new file mode 100644 index 00000000..71d7f7aa --- /dev/null +++ b/rstest/rsbuild-adapter/tsconfig.json @@ -0,0 +1,17 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@utils/*": ["./src/utils/*"] + }, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts", "rsbuild.config.ts"] +} diff --git a/rstest/rslib-adapter/package.json b/rstest/rslib-adapter/package.json new file mode 100644 index 00000000..024f5ff2 --- /dev/null +++ b/rstest/rslib-adapter/package.json @@ -0,0 +1,16 @@ +{ + "name": "rstest-rslib-adapter", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "build": "rslib build", + "test": "rstest run" + }, + "devDependencies": { + "@rslib/core": "^0.19.1", + "@rstest/adapter-rslib": "^0.1.1", + "@rstest/core": "^0.7.9", + "typescript": "^5.8.3" + } +} diff --git a/rstest/rslib-adapter/rslib.config.ts b/rstest/rslib-adapter/rslib.config.ts new file mode 100644 index 00000000..80188408 --- /dev/null +++ b/rstest/rslib-adapter/rslib.config.ts @@ -0,0 +1,34 @@ +import { defineConfig } from '@rslib/core'; + +export default defineConfig({ + lib: [ + { + id: 'esm', + format: 'esm', + syntax: 'es2021', + dts: true, + }, + { + id: 'cjs', + format: 'cjs', + syntax: 'es2021', + }, + ], + source: { + entry: { + index: './src/index.ts', + utils: './src/utils.ts', + }, + // Define compile-time constants + define: { + __VERSION__: JSON.stringify('1.0.0'), + __DEV__: JSON.stringify(true), + 'process.env.LIB_NAME': JSON.stringify('rstest-rslib-adapter'), + }, + // Alias configuration + alias: { + '@': './src', + '@utils': './src/utils.ts', + }, + }, +}); diff --git a/rstest/rslib-adapter/rstest.config.ts b/rstest/rslib-adapter/rstest.config.ts new file mode 100644 index 00000000..603c456c --- /dev/null +++ b/rstest/rslib-adapter/rstest.config.ts @@ -0,0 +1,17 @@ +import { withRslibConfig } from '@rstest/adapter-rslib'; +import { defineConfig, type ExtendConfigFn } from '@rstest/core'; + +export default defineConfig({ + // Type assertion needed due to adapter package version mismatch + extends: withRslibConfig({ + libId: 'esm', + modifyLibConfig: (config) => { + config.source ??= {}; + config.source.alias = { + ...config.source.alias, + '@test-utils': './tests/test-utils.ts', + }; + return config; + }, + }) as ExtendConfigFn, +}); diff --git a/rstest/rslib-adapter/src/index.ts b/rstest/rslib-adapter/src/index.ts new file mode 100644 index 00000000..cf73f6d0 --- /dev/null +++ b/rstest/rslib-adapter/src/index.ts @@ -0,0 +1,27 @@ +// Declare compile-time constants +declare const __VERSION__: string; +declare const __DEV__: boolean; + +/** + * Get library version from define + */ +export function getVersion(): string { + return __VERSION__; +} + +/** + * Check if in development mode + */ +export function isDev(): boolean { + return __DEV__; +} + +/** + * Get library name from environment + */ +export function getLibName(): string { + return process.env.LIB_NAME || 'unknown'; +} + +// Re-export utilities +export * from '@utils'; diff --git a/rstest/rslib-adapter/src/utils.ts b/rstest/rslib-adapter/src/utils.ts new file mode 100644 index 00000000..ccce9b15 --- /dev/null +++ b/rstest/rslib-adapter/src/utils.ts @@ -0,0 +1,86 @@ +/** + * Utility functions for the library + */ + +/** + * Adds two numbers together + */ +export function add(a: number, b: number): number { + return a + b; +} + +/** + * Subtracts b from a + */ +export function subtract(a: number, b: number): number { + return a - b; +} + +/** + * Multiplies two numbers + */ +export function multiply(a: number, b: number): number { + return a * b; +} + +/** + * Divides a by b + * @throws Error if b is zero + */ +export function divide(a: number, b: number): number { + if (b === 0) { + throw new Error('Division by zero'); + } + return a / b; +} + +/** + * Clamps a value between min and max + */ +export function clamp(value: number, min: number, max: number): number { + return Math.min(Math.max(value, min), max); +} + +/** + * Deep clones an object + */ +export function deepClone(obj: T): T { + return JSON.parse(JSON.stringify(obj)); +} + +/** + * Debounces a function + */ +export function debounce unknown>( + fn: T, + delay: number, +): (...args: Parameters) => void { + let timeoutId: ReturnType | null = null; + + return (...args: Parameters) => { + if (timeoutId) { + clearTimeout(timeoutId); + } + timeoutId = setTimeout(() => { + fn(...args); + }, delay); + }; +} + +/** + * Throttles a function + */ +export function throttle unknown>( + fn: T, + delay: number, +): (...args: Parameters) => void { + let lastCall = 0; + + return (...args: Parameters) => { + const now = Date.now(); + if (now - lastCall >= delay) { + lastCall = now; + fn(...args); + } + }; +} diff --git a/rstest/rslib-adapter/tests/index.test.ts b/rstest/rslib-adapter/tests/index.test.ts new file mode 100644 index 00000000..eab9a867 --- /dev/null +++ b/rstest/rslib-adapter/tests/index.test.ts @@ -0,0 +1,43 @@ +import { describe, expect, it } from '@rstest/core'; +import { getLibName, getVersion, isDev } from '@/index'; + +describe('Define - Compile-time Constants', () => { + /** + * Tests that the `define` configuration in rslib.config.ts is inherited + * by rstest via withRslibConfig adapter + */ + + it('should get version from __VERSION__ define', () => { + const version = getVersion(); + expect(version).toBe('1.0.0'); + }); + + it('should get dev mode from __DEV__ define', () => { + const dev = isDev(); + expect(dev).toBe(true); + }); + + it('should get lib name from process.env.LIB_NAME define', () => { + const libName = getLibName(); + expect(libName).toBe('rstest-rslib-adapter'); + }); +}); + +describe('Alias - Path Resolution', () => { + /** + * Tests that the `alias` configuration in rslib.config.ts is inherited + * by rstest via withRslibConfig adapter + */ + + it('should resolve @ alias to src directory', async () => { + // This import uses the @ alias configured in rslib.config.ts + const { add } = await import('@/index'); + expect(add(1, 2)).toBe(3); + }); + + it('should resolve @utils alias', async () => { + // This import uses the @utils alias + const { multiply } = await import('@utils'); + expect(multiply(3, 4)).toBe(12); + }); +}); diff --git a/rstest/rslib-adapter/tests/modify-config.test.ts b/rstest/rslib-adapter/tests/modify-config.test.ts new file mode 100644 index 00000000..8756f2b6 --- /dev/null +++ b/rstest/rslib-adapter/tests/modify-config.test.ts @@ -0,0 +1,34 @@ +import { describe, expect, it } from '@rstest/core'; +import { createTestContext, waitFor } from '@test-utils'; + +describe('modifyLibConfig - Custom Test Aliases', () => { + /** + * Tests that the modifyLibConfig option in withRslibConfig + * allows adding custom aliases for testing + */ + + it('should resolve @test-utils alias added via modifyLibConfig', () => { + // The @test-utils alias is added in rstest.config.ts via modifyLibConfig + const ctx = createTestContext({ value: 0 }); + + expect(ctx.get()).toEqual({ value: 0 }); + }); + + it('should use test utilities correctly', () => { + const ctx = createTestContext({ name: 'test', count: 0 }); + + ctx.set({ count: 5 }); + expect(ctx.get()).toEqual({ name: 'test', count: 5 }); + + ctx.reset(); + expect(ctx.get()).toEqual({ name: 'test', count: 0 }); + }); + + it('should support async test utilities', async () => { + const start = Date.now(); + await waitFor(50); + const elapsed = Date.now() - start; + + expect(elapsed).toBeGreaterThanOrEqual(45); + }); +}); diff --git a/rstest/rslib-adapter/tests/test-utils.ts b/rstest/rslib-adapter/tests/test-utils.ts new file mode 100644 index 00000000..29c7278c --- /dev/null +++ b/rstest/rslib-adapter/tests/test-utils.ts @@ -0,0 +1,21 @@ +/** + * Test utilities for rslib-adapter example + */ + +export function createTestContext(initial: T) { + let context = { ...initial }; + + return { + get: () => context, + set: (partial: Partial) => { + context = { ...context, ...partial }; + }, + reset: () => { + context = { ...initial }; + }, + }; +} + +export function waitFor(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/rstest/rslib-adapter/tests/utils.test.ts b/rstest/rslib-adapter/tests/utils.test.ts new file mode 100644 index 00000000..961f3477 --- /dev/null +++ b/rstest/rslib-adapter/tests/utils.test.ts @@ -0,0 +1,115 @@ +import { describe, expect, it } from '@rstest/core'; +import { add, clamp, deepClone, divide, multiply, subtract } from '@utils'; + +describe('Utils - Basic Math Operations', () => { + describe('add', () => { + it('should add two positive numbers', () => { + expect(add(2, 3)).toBe(5); + }); + + it('should add negative numbers', () => { + expect(add(-2, -3)).toBe(-5); + }); + + it('should add mixed numbers', () => { + expect(add(-2, 5)).toBe(3); + }); + + it('should handle zero', () => { + expect(add(0, 5)).toBe(5); + expect(add(5, 0)).toBe(5); + }); + + it('should handle decimals', () => { + expect(add(0.1, 0.2)).toBeCloseTo(0.3); + }); + }); + + describe('subtract', () => { + it('should subtract two numbers', () => { + expect(subtract(5, 3)).toBe(2); + }); + + it('should handle negative result', () => { + expect(subtract(3, 5)).toBe(-2); + }); + }); + + describe('multiply', () => { + it('should multiply two numbers', () => { + expect(multiply(3, 4)).toBe(12); + }); + + it('should handle zero', () => { + expect(multiply(5, 0)).toBe(0); + }); + + it('should handle negative numbers', () => { + expect(multiply(-3, 4)).toBe(-12); + expect(multiply(-3, -4)).toBe(12); + }); + }); + + describe('divide', () => { + it('should divide two numbers', () => { + expect(divide(10, 2)).toBe(5); + }); + + it('should handle decimal result', () => { + expect(divide(5, 2)).toBe(2.5); + }); + + it('should throw on division by zero', () => { + expect(() => divide(10, 0)).toThrow('Division by zero'); + }); + }); +}); + +describe('Utils - Helper Functions', () => { + describe('clamp', () => { + it('should return value when within range', () => { + expect(clamp(5, 0, 10)).toBe(5); + }); + + it('should clamp to min', () => { + expect(clamp(-5, 0, 10)).toBe(0); + }); + + it('should clamp to max', () => { + expect(clamp(15, 0, 10)).toBe(10); + }); + + it('should handle edge cases', () => { + expect(clamp(0, 0, 10)).toBe(0); + expect(clamp(10, 0, 10)).toBe(10); + }); + }); + + describe('deepClone', () => { + it('should clone simple objects', () => { + const obj = { a: 1, b: 2 }; + const cloned = deepClone(obj); + + expect(cloned).toEqual(obj); + expect(cloned).not.toBe(obj); + }); + + it('should clone nested objects', () => { + const obj = { a: { b: { c: 1 } } }; + const cloned = deepClone(obj); + + expect(cloned).toEqual(obj); + expect(cloned.a).not.toBe(obj.a); + expect(cloned.a.b).not.toBe(obj.a.b); + }); + + it('should clone arrays', () => { + const arr = [1, 2, [3, 4]]; + const cloned = deepClone(arr); + + expect(cloned).toEqual(arr); + expect(cloned).not.toBe(arr); + expect(cloned[2]).not.toBe(arr[2]); + }); + }); +}); diff --git a/rstest/rslib-adapter/tsconfig.json b/rstest/rslib-adapter/tsconfig.json new file mode 100644 index 00000000..d774a831 --- /dev/null +++ b/rstest/rslib-adapter/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "outDir": "dist", + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"], + "@utils": ["./src/utils.ts"], + "@test-utils": ["./tests/test-utils.ts"] + }, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts", "rslib.config.ts"] +} diff --git a/rstest/snapshot/package.json b/rstest/snapshot/package.json new file mode 100644 index 00000000..81ae689f --- /dev/null +++ b/rstest/snapshot/package.json @@ -0,0 +1,13 @@ +{ + "name": "rstest-snapshot", + "version": "1.0.0", + "private": true, + "scripts": { + "test": "rstest run", + "test:update": "rstest run --update" + }, + "devDependencies": { + "@rstest/core": "^0.7.9", + "typescript": "^5.8.3" + } +} diff --git a/rstest/snapshot/rstest.config.ts b/rstest/snapshot/rstest.config.ts new file mode 100644 index 00000000..80b4dc1e --- /dev/null +++ b/rstest/snapshot/rstest.config.ts @@ -0,0 +1,10 @@ +import path from 'node:path'; +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + resolveSnapshotPath: (testPath, snapshotExtension) => { + const testDir = path.dirname(testPath); + const testFileName = path.basename(testPath); + return path.join(testDir, '__snapshots__', testFileName + snapshotExtension); + }, +}); diff --git a/rstest/snapshot/src/config.ts b/rstest/snapshot/src/config.ts new file mode 100644 index 00000000..b1ca2454 --- /dev/null +++ b/rstest/snapshot/src/config.ts @@ -0,0 +1,79 @@ +/** + * Config interface for snapshot testing + */ +export interface AppConfig { + app: { + name: string; + version: string; + debug: boolean; + }; + database: { + host: string; + port: number; + name: string; + pool: { + min: number; + max: number; + }; + }; + features: string[]; + logging: { + level: 'debug' | 'info' | 'warn' | 'error'; + format: 'json' | 'text'; + }; +} + +/** + * Create default config + */ +export function createConfig(overrides: Partial = {}): AppConfig { + return { + app: { + name: 'MyApp', + version: '1.0.0', + debug: false, + ...overrides.app, + }, + database: { + host: 'localhost', + port: 5432, + name: 'myapp_db', + pool: { + min: 2, + max: 10, + ...overrides.database?.pool, + }, + ...overrides.database, + }, + features: overrides.features ?? ['auth', 'api', 'websocket'], + logging: { + level: 'info', + format: 'json', + ...overrides.logging, + }, + }; +} + +/** + * Validate config + */ +export function validateConfig(config: AppConfig): { valid: boolean; errors: string[] } { + const errors: string[] = []; + + if (!config.app.name) { + errors.push('app.name is required'); + } + + if (config.database.port < 1 || config.database.port > 65535) { + errors.push('database.port must be between 1 and 65535'); + } + + if (config.database.pool.min > config.database.pool.max) { + errors.push('database.pool.min must be <= database.pool.max'); + } + + return { + valid: errors.length === 0, + errors, + }; +} diff --git a/rstest/snapshot/src/html.ts b/rstest/snapshot/src/html.ts new file mode 100644 index 00000000..ec9c455f --- /dev/null +++ b/rstest/snapshot/src/html.ts @@ -0,0 +1,62 @@ +/** + * HTML template generator for snapshot testing + */ + +export interface ComponentProps { + id?: string; + className?: string; + children?: string; + attributes?: Record; +} + +/** + * Generate a div element HTML + */ +export function div(props: ComponentProps = {}): string { + const { id, className, children = '', attributes = {} } = props; + + const attrs = [ + id ? `id="${id}"` : '', + className ? `class="${className}"` : '', + ...Object.entries(attributes).map(([key, value]) => `${key}="${value}"`), + ] + .filter(Boolean) + .join(' '); + + return `${children}`; +} + +/** + * Generate a button element HTML + */ +export function button(props: ComponentProps & { type?: string; disabled?: boolean } = {}): string { + const { + id, + className, + children = '', + type = 'button', + disabled = false, + attributes = {}, + } = props; + + const attrs = [ + `type="${type}"`, + id ? `id="${id}"` : '', + className ? `class="${className}"` : '', + disabled ? 'disabled' : '', + ...Object.entries(attributes).map(([key, value]) => `${key}="${value}"`), + ] + .filter(Boolean) + .join(' '); + + return ``; +} + +/** + * Generate a list HTML + */ +export function list(items: string[], ordered = false): string { + const tag = ordered ? 'ol' : 'ul'; + const listItems = items.map((item) => `
  • ${item}
  • `).join('\n'); + return `<${tag}>\n${listItems}\n`; +} diff --git a/rstest/snapshot/src/index.ts b/rstest/snapshot/src/index.ts new file mode 100644 index 00000000..393aa880 --- /dev/null +++ b/rstest/snapshot/src/index.ts @@ -0,0 +1,3 @@ +export * from './config'; +export * from './html'; +export * from './user'; diff --git a/rstest/snapshot/src/user.ts b/rstest/snapshot/src/user.ts new file mode 100644 index 00000000..4b05cacc --- /dev/null +++ b/rstest/snapshot/src/user.ts @@ -0,0 +1,45 @@ +/** + * User interface for snapshot testing + */ +export interface User { + id: number; + name: string; + email: string; + role: 'admin' | 'user' | 'guest'; + createdAt: Date; + metadata?: Record; +} + +/** + * Create a user object + */ +export function createUser(overrides: Partial = {}): User { + return { + id: 1, + name: 'John Doe', + email: 'john@example.com', + role: 'user', + createdAt: new Date('2024-01-01T00:00:00.000Z'), + ...overrides, + }; +} + +/** + * Format user for display + */ +export function formatUser(user: User): string { + return `${user.name} <${user.email}> (${user.role})`; +} + +/** + * Generate user list + */ +export function generateUsers(count: number): User[] { + return Array.from({ length: count }, (_, i) => + createUser({ + id: i + 1, + name: `User ${i + 1}`, + email: `user${i + 1}@example.com`, + }), + ); +} diff --git a/rstest/snapshot/tests/__snapshots__/basic.test.ts.snap b/rstest/snapshot/tests/__snapshots__/basic.test.ts.snap new file mode 100644 index 00000000..ec3248f4 --- /dev/null +++ b/rstest/snapshot/tests/__snapshots__/basic.test.ts.snap @@ -0,0 +1,119 @@ +// Rstest Snapshot v1 + +exports[`toMatchSnapshot - Basic Usage > should match array snapshot 1`] = ` +[ + { + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "user1@example.com", + "id": 1, + "name": "User 1", + "role": "user", + }, + { + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "user2@example.com", + "id": 2, + "name": "User 2", + "role": "user", + }, + { + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "user3@example.com", + "id": 3, + "name": "User 3", + "role": "user", + }, +] +`; + +exports[`toMatchSnapshot - Basic Usage > should match formatted user snapshot 1`] = `"Jane Doe (user)"`; + +exports[`toMatchSnapshot - Basic Usage > should match user object snapshot 1`] = ` +{ + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "john@example.com", + "id": 1, + "name": "John Doe", + "role": "user", +} +`; + +exports[`toMatchSnapshot - Basic Usage > should match with custom snapshot name > admin user 1`] = ` +{ + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "john@example.com", + "id": 1, + "name": "John Doe", + "role": "admin", +} +`; + +exports[`toMatchSnapshot - Complex Objects > should match nested object snapshot 1`] = ` +{ + "meta": { + "generatedAt": "2024-01-01T00:00:00.000Z", + "version": "1.0.0", + }, + "pagination": { + "page": 1, + "perPage": 10, + "total": 100, + }, + "users": [ + { + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "user1@example.com", + "id": 1, + "name": "User 1", + "role": "user", + }, + { + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "user2@example.com", + "id": 2, + "name": "User 2", + "role": "user", + }, + ], +} +`; + +exports[`toMatchSnapshot - Complex Objects > should match user with metadata snapshot 1`] = ` +{ + "createdAt": 2024-01-01T00:00:00.000Z, + "email": "john@example.com", + "id": 1, + "metadata": { + "lastLogin": "2024-01-15T10:30:00.000Z", + "preferences": { + "language": "en", + "theme": "dark", + }, + }, + "name": "John Doe", + "role": "user", +} +`; + +exports[`toMatchSnapshot - Property Matchers > should match array with property matchers 1`] = ` +[ + { + "id": Any, + "name": "User 1", + }, + { + "id": Any, + "name": "User 2", + }, +] +`; + +exports[`toMatchSnapshot - Property Matchers > should match with property matchers for dynamic values 1`] = ` +{ + "createdAt": Any, + "email": "test@example.com", + "id": Any, + "name": "Test User", + "role": "user", +} +`; diff --git a/rstest/snapshot/tests/basic.test.ts b/rstest/snapshot/tests/basic.test.ts new file mode 100644 index 00000000..a6e81d40 --- /dev/null +++ b/rstest/snapshot/tests/basic.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from '@rstest/core'; +import { createUser, formatUser, generateUsers, type User } from '../src/user'; + +describe('toMatchSnapshot - Basic Usage', () => { + /** + * toMatchSnapshot() creates a snapshot file on first run, + * then compares against it on subsequent runs + */ + + it('should match user object snapshot', () => { + const user = createUser(); + + // Snapshot will be saved to __snapshots__/basic.test.ts.snap + expect(user).toMatchSnapshot(); + }); + + it('should match formatted user snapshot', () => { + const user = createUser({ name: 'Jane Doe', email: 'jane@example.com' }); + const formatted = formatUser(user); + + expect(formatted).toMatchSnapshot(); + }); + + it('should match array snapshot', () => { + const users = generateUsers(3); + + expect(users).toMatchSnapshot(); + }); + + it('should match with custom snapshot name', () => { + const user = createUser({ role: 'admin' }); + + // Use a custom name for the snapshot + expect(user).toMatchSnapshot('admin user'); + }); +}); + +describe('toMatchSnapshot - Complex Objects', () => { + it('should match nested object snapshot', () => { + const data = { + users: generateUsers(2), + pagination: { + page: 1, + perPage: 10, + total: 100, + }, + meta: { + generatedAt: '2024-01-01T00:00:00.000Z', + version: '1.0.0', + }, + }; + + expect(data).toMatchSnapshot(); + }); + + it('should match user with metadata snapshot', () => { + const user = createUser({ + metadata: { + preferences: { + theme: 'dark', + language: 'en', + }, + lastLogin: '2024-01-15T10:30:00.000Z', + }, + }); + + expect(user).toMatchSnapshot(); + }); +}); + +describe('toMatchSnapshot - Property Matchers', () => { + /** + * Use property matchers for dynamic values like dates or IDs + */ + + it('should match with property matchers for dynamic values', () => { + const user: User = { + id: Math.random() * 1000, // Dynamic ID + name: 'Test User', + email: 'test@example.com', + role: 'user', + createdAt: new Date(), // Dynamic date + }; + + expect(user).toMatchSnapshot({ + id: expect.any(Number), + createdAt: expect.any(Date), + }); + }); + + it('should match array with property matchers', () => { + const users = [ + { id: Math.random(), name: 'User 1' }, + { id: Math.random(), name: 'User 2' }, + ]; + + expect(users).toMatchSnapshot([ + { id: expect.any(Number), name: 'User 1' }, + { id: expect.any(Number), name: 'User 2' }, + ]); + }); +}); diff --git a/rstest/snapshot/tests/file.test.ts b/rstest/snapshot/tests/file.test.ts new file mode 100644 index 00000000..ddc05987 --- /dev/null +++ b/rstest/snapshot/tests/file.test.ts @@ -0,0 +1,109 @@ +import { describe, expect, it } from '@rstest/core'; +import { createConfig, validateConfig } from '../src/config'; + +describe('toMatchFileSnapshot - Basic Usage', () => { + /** + * toMatchFileSnapshot() saves the snapshot to a separate file + * that you specify. Useful for large outputs or when you want + * to organize snapshots in a specific way. + */ + + it('should match config file snapshot', async () => { + const config = createConfig(); + + // Save to a specific file + await expect(JSON.stringify(config, null, 2)).toMatchFileSnapshot( + './tests/__snapshots__/config.json', + ); + }); + + it('should match config with overrides file snapshot', async () => { + const config = createConfig({ + app: { name: 'CustomApp', version: '2.0.0', debug: true }, + features: ['custom-feature'], + }); + + await expect(JSON.stringify(config, null, 2)).toMatchFileSnapshot( + './tests/__snapshots__/custom-config.json', + ); + }); +}); + +describe('toMatchFileSnapshot - Different Formats', () => { + it('should match YAML-like format file snapshot', async () => { + const config = createConfig(); + + // Format as YAML-like string + const yaml = `app: + name: ${config.app.name} + version: ${config.app.version} + debug: ${config.app.debug} +database: + host: ${config.database.host} + port: ${config.database.port} + name: ${config.database.name} + pool: + min: ${config.database.pool.min} + max: ${config.database.pool.max} +features: +${config.features.map((f) => ` - ${f}`).join('\n')} +logging: + level: ${config.logging.level} + format: ${config.logging.format}`; + + await expect(yaml).toMatchFileSnapshot('./tests/__snapshots__/config.yaml'); + }); + + it('should match validation result file snapshot', async () => { + const config = createConfig({ + database: { + host: 'localhost', + port: 70000, // Invalid port + name: 'test', + pool: { min: 10, max: 5 }, // Invalid: min > max + }, + }); + + const result = validateConfig(config); + + await expect(JSON.stringify(result, null, 2)).toMatchFileSnapshot( + './tests/__snapshots__/validation-errors.json', + ); + }); +}); + +describe('toMatchFileSnapshot - HTML Output', () => { + it('should match HTML page file snapshot', async () => { + const html = ` + + + + + Test Page + + +
    +

    Welcome

    + +
    +
    +
    +

    Content

    +

    This is the main content area.

    +
    +
    +
    +

    © 2024 Test Company

    +
    + +`; + + await expect(html).toMatchFileSnapshot('./tests/__snapshots__/page.html'); + }); +}); diff --git a/rstest/snapshot/tests/inline.test.ts b/rstest/snapshot/tests/inline.test.ts new file mode 100644 index 00000000..2d3a757d --- /dev/null +++ b/rstest/snapshot/tests/inline.test.ts @@ -0,0 +1,93 @@ +import { describe, expect, it } from '@rstest/core'; +import { button, div, list } from '../src/html'; + +describe('toMatchInlineSnapshot - Basic Usage', () => { + /** + * toMatchInlineSnapshot() stores the snapshot directly in the test file. + * On first run, it will update the test file with the snapshot. + * Run with --update flag to update existing inline snapshots. + */ + + it('should match div inline snapshot', () => { + const html = div({ className: 'container', children: 'Hello World' }); + + expect(html).toMatchInlineSnapshot(`"
    Hello World
    "`); + }); + + it('should match button inline snapshot', () => { + const html = button({ + id: 'submit-btn', + className: 'btn btn-primary', + children: 'Submit', + }); + + expect(html).toMatchInlineSnapshot( + `""`, + ); + }); + + it('should match disabled button inline snapshot', () => { + const html = button({ + className: 'btn', + children: 'Disabled', + disabled: true, + }); + + expect(html).toMatchInlineSnapshot( + `""`, + ); + }); +}); + +describe('toMatchInlineSnapshot - Lists', () => { + it('should match unordered list inline snapshot', () => { + const html = list(['Item 1', 'Item 2', 'Item 3']); + + expect(html).toMatchInlineSnapshot(` +"
      +
    • Item 1
    • +
    • Item 2
    • +
    • Item 3
    • +
    " +`); + }); + + it('should match ordered list inline snapshot', () => { + const html = list(['First', 'Second', 'Third'], true); + + expect(html).toMatchInlineSnapshot(` +"
      +
    1. First
    2. +
    3. Second
    4. +
    5. Third
    6. +
    " +`); + }); +}); + +describe('toMatchInlineSnapshot - Objects', () => { + it('should match simple object inline snapshot', () => { + const obj = { name: 'test', value: 42 }; + + expect(obj).toMatchInlineSnapshot(` +{ + "name": "test", + "value": 42, +} +`); + }); + + it('should match array inline snapshot', () => { + const arr = [1, 2, 3, 4, 5]; + + expect(arr).toMatchInlineSnapshot(` +[ + 1, + 2, + 3, + 4, + 5, +] +`); + }); +}); diff --git a/rstest/snapshot/tests/serializer.test.ts b/rstest/snapshot/tests/serializer.test.ts new file mode 100644 index 00000000..b5d28fde --- /dev/null +++ b/rstest/snapshot/tests/serializer.test.ts @@ -0,0 +1,128 @@ +import { afterAll, beforeAll, describe, expect, it } from '@rstest/core'; +import { createUser, type User } from '../src/user'; + +describe('Custom Snapshot Serializer', () => { + /** + * expect.addSnapshotSerializer() allows you to customize + * how specific types are serialized in snapshots + */ + + beforeAll(() => { + // Add custom serializer for User objects + expect.addSnapshotSerializer({ + // Check if this serializer should handle the value + test: (val): val is User => { + return ( + val !== null && + typeof val === 'object' && + 'id' in val && + 'name' in val && + 'email' in val && + 'role' in val + ); + }, + // Serialize the value + serialize: (val: User, config, indentation, _depth, _refs, _printer) => { + const indent = indentation + config.indent; + return `User {\n${indent}id: ${val.id}\n${indent}name: "${val.name}"\n${indent}email: "${val.email}"\n${indent}role: "${val.role}"\n${indentation}}`; + }, + }); + }); + + afterAll(() => { + // Note: In a real scenario, you might want to reset serializers + // rstest provides ways to manage this + }); + + it('should use custom serializer for User', () => { + const user = createUser({ + id: 1, + name: 'Alice', + email: 'alice@example.com', + role: 'admin', + }); + + expect(user).toMatchInlineSnapshot(` +User { + id: 1 + name: "Alice" + email: "alice@example.com" + role: "admin" +} +`); + }); + + it('should use custom serializer for another User', () => { + const user = createUser({ + id: 2, + name: 'Bob', + email: 'bob@example.com', + role: 'guest', + }); + + expect(user).toMatchInlineSnapshot(` +User { + id: 2 + name: "Bob" + email: "bob@example.com" + role: "guest" +} +`); + }); +}); + +describe('Custom Serializer - Date Formatting', () => { + beforeAll(() => { + // Add custom serializer for Date objects + expect.addSnapshotSerializer({ + test: (val): val is Date => val instanceof Date, + serialize: (val: Date) => { + return `Date<${val.toISOString().split('T')[0]}>`; + }, + }); + }); + + it('should format dates nicely', () => { + const date = new Date('2024-06-15T10:30:00.000Z'); + + expect(date).toMatchInlineSnapshot(`Date<2024-06-15>`); + }); + + it('should format dates in objects', () => { + const event = { + name: 'Conference', + date: new Date('2024-12-01T09:00:00.000Z'), + }; + + expect(event).toMatchInlineSnapshot(` +{ + "date": Date<2024-12-01>, + "name": "Conference", +} +`); + }); +}); + +describe('Custom Serializer - Error Objects', () => { + beforeAll(() => { + // Add custom serializer for Error objects + expect.addSnapshotSerializer({ + test: (val): val is Error => val instanceof Error, + serialize: (val: Error) => { + return `[Error: ${val.message}]`; + }, + }); + }); + + it('should serialize errors cleanly', () => { + const error = new Error('Something went wrong'); + + expect(error).toMatchInlineSnapshot(`[Error: Something went wrong]`); + }); + + it('should serialize TypeError', () => { + const error = new TypeError('Invalid type provided'); + + expect(error).toMatchInlineSnapshot(`[Error: Invalid type provided]`); + }); +}); diff --git a/rstest/snapshot/tests/tests/__snapshots__/config.json b/rstest/snapshot/tests/tests/__snapshots__/config.json new file mode 100644 index 00000000..635405dd --- /dev/null +++ b/rstest/snapshot/tests/tests/__snapshots__/config.json @@ -0,0 +1,25 @@ +{ + "app": { + "name": "MyApp", + "version": "1.0.0", + "debug": false + }, + "database": { + "host": "localhost", + "port": 5432, + "name": "myapp_db", + "pool": { + "min": 2, + "max": 10 + } + }, + "features": [ + "auth", + "api", + "websocket" + ], + "logging": { + "level": "info", + "format": "json" + } +} \ No newline at end of file diff --git a/rstest/snapshot/tests/tests/__snapshots__/config.yaml b/rstest/snapshot/tests/tests/__snapshots__/config.yaml new file mode 100644 index 00000000..37a01d2c --- /dev/null +++ b/rstest/snapshot/tests/tests/__snapshots__/config.yaml @@ -0,0 +1,18 @@ +app: + name: MyApp + version: 1.0.0 + debug: false +database: + host: localhost + port: 5432 + name: myapp_db + pool: + min: 2 + max: 10 +features: + - auth + - api + - websocket +logging: + level: info + format: json \ No newline at end of file diff --git a/rstest/snapshot/tests/tests/__snapshots__/custom-config.json b/rstest/snapshot/tests/tests/__snapshots__/custom-config.json new file mode 100644 index 00000000..952f3081 --- /dev/null +++ b/rstest/snapshot/tests/tests/__snapshots__/custom-config.json @@ -0,0 +1,23 @@ +{ + "app": { + "name": "CustomApp", + "version": "2.0.0", + "debug": true + }, + "database": { + "host": "localhost", + "port": 5432, + "name": "myapp_db", + "pool": { + "min": 2, + "max": 10 + } + }, + "features": [ + "custom-feature" + ], + "logging": { + "level": "info", + "format": "json" + } +} \ No newline at end of file diff --git a/rstest/snapshot/tests/tests/__snapshots__/page.html b/rstest/snapshot/tests/tests/__snapshots__/page.html new file mode 100644 index 00000000..8be5646f --- /dev/null +++ b/rstest/snapshot/tests/tests/__snapshots__/page.html @@ -0,0 +1,29 @@ + + + + + + Test Page + + +
    +

    Welcome

    + +
    +
    +
    +

    Content

    +

    This is the main content area.

    +
    +
    + + + \ No newline at end of file diff --git a/rstest/snapshot/tests/tests/__snapshots__/validation-errors.json b/rstest/snapshot/tests/tests/__snapshots__/validation-errors.json new file mode 100644 index 00000000..21d58965 --- /dev/null +++ b/rstest/snapshot/tests/tests/__snapshots__/validation-errors.json @@ -0,0 +1,7 @@ +{ + "valid": false, + "errors": [ + "database.port must be between 1 and 65535", + "database.pool.min must be <= database.pool.max" + ] +} \ No newline at end of file diff --git a/rstest/snapshot/tsconfig.json b/rstest/snapshot/tsconfig.json new file mode 100644 index 00000000..5d1c2508 --- /dev/null +++ b/rstest/snapshot/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "declaration": true, + "types": ["@rstest/core"] + }, + "include": ["src", "tests", "rstest.config.ts"] +}