From 0da9299daf3073ed771763bf104af3da4a3eb1b4 Mon Sep 17 00:00:00 2001 From: Wei Date: Tue, 1 Jul 2025 15:00:43 +0800 Subject: [PATCH 01/10] feat: more mock api (#290) --- biome.json | 5 +- packages/core/src/core/plugins/entry.ts | 2 +- packages/core/src/core/plugins/mockRuntime.ts | 90 +++++++++++++++++++ packages/core/src/core/rsbuild.ts | 13 ++- packages/core/src/runtime/api/index.ts | 1 + packages/core/src/runtime/api/utilities.ts | 29 ++++-- .../core/src/runtime/worker/loadModule.ts | 6 ++ packages/core/src/types/mock.ts | 33 ++++--- .../core/__snapshots__/rsbuild.test.ts.snap | 8 +- pnpm-lock.yaml | 85 ++++++++++++++++++ tests/__mocks__/axios.ts | 6 ++ tests/__mocks__/fs.js | 6 ++ tests/__mocks__/redux.ts | 6 ++ tests/mock/fixtures/unmock/noUnmock.test.ts | 6 ++ tests/mock/fixtures/unmock/rstest.config.ts | 7 ++ tests/mock/fixtures/unmock/rstest.setup.ts | 9 ++ tests/mock/fixtures/unmock/unmock.test.ts | 8 ++ tests/mock/src/increment.ts | 3 + tests/mock/src/mockedDependency.ts | 3 + tests/mock/src/readSomeFile.ts | 5 ++ tests/mock/tests/doMock.test.ts | 58 ++++++++++++ tests/mock/tests/doMockRequire.test.ts | 32 +++++++ tests/mock/tests/doUnmock.test.ts | 29 ++++++ tests/mock/tests/external.test.ts | 15 ---- tests/mock/tests/importActual.test.ts | 20 +++++ tests/mock/tests/index.test.ts | 13 --- tests/mock/tests/loadMock.test.ts | 23 +++++ tests/mock/tests/mock.test.ts | 33 +++++++ tests/mock/tests/mockNodeProtocol.test.ts | 13 +++ tests/mock/tests/mockNonResolved.test.ts | 26 ++++++ tests/mock/tests/mockRequire.test.ts | 11 +++ tests/mock/tests/reduxMocked.test.ts | 25 ++++++ tests/mock/tests/requireActual.test.ts | 19 ++++ tests/mock/tests/resetModules.test.ts | 10 +++ tests/mock/tests/unmock.test.ts | 22 +++++ tests/package.json | 3 + tests/scripts/index.ts | 2 +- tests/test-api/edgeCase.test.ts | 4 +- tests/tsconfig.json | 3 +- 39 files changed, 630 insertions(+), 62 deletions(-) create mode 100644 packages/core/src/core/plugins/mockRuntime.ts create mode 100644 tests/__mocks__/axios.ts create mode 100644 tests/__mocks__/fs.js create mode 100644 tests/__mocks__/redux.ts create mode 100644 tests/mock/fixtures/unmock/noUnmock.test.ts create mode 100644 tests/mock/fixtures/unmock/rstest.config.ts create mode 100644 tests/mock/fixtures/unmock/rstest.setup.ts create mode 100644 tests/mock/fixtures/unmock/unmock.test.ts create mode 100644 tests/mock/src/increment.ts create mode 100644 tests/mock/src/mockedDependency.ts create mode 100644 tests/mock/src/readSomeFile.ts create mode 100644 tests/mock/tests/doMock.test.ts create mode 100644 tests/mock/tests/doMockRequire.test.ts create mode 100644 tests/mock/tests/doUnmock.test.ts delete mode 100644 tests/mock/tests/external.test.ts create mode 100644 tests/mock/tests/importActual.test.ts delete mode 100644 tests/mock/tests/index.test.ts create mode 100644 tests/mock/tests/loadMock.test.ts create mode 100644 tests/mock/tests/mock.test.ts create mode 100644 tests/mock/tests/mockNodeProtocol.test.ts create mode 100644 tests/mock/tests/mockNonResolved.test.ts create mode 100644 tests/mock/tests/mockRequire.test.ts create mode 100644 tests/mock/tests/reduxMocked.test.ts create mode 100644 tests/mock/tests/requireActual.test.ts create mode 100644 tests/mock/tests/resetModules.test.ts create mode 100644 tests/mock/tests/unmock.test.ts diff --git a/biome.json b/biome.json index 435c860b..303388ce 100644 --- a/biome.json +++ b/biome.json @@ -8,10 +8,10 @@ "useIgnoreFile": true }, "files": { - "ignoreUnknown": true + "ignoreUnknown": true, + "includes": ["**", "!**/*.vue", "!**/dist/**"] }, "formatter": { - "includes": ["**", "!**/*.vue"], "indentStyle": "space" }, "javascript": { @@ -32,7 +32,6 @@ }, "linter": { "enabled": true, - "includes": ["**", "!**/*.vue"], "rules": { "recommended": true, "style": { diff --git a/packages/core/src/core/plugins/entry.ts b/packages/core/src/core/plugins/entry.ts index 4f720f10..448193fd 100644 --- a/packages/core/src/core/plugins/entry.ts +++ b/packages/core/src/core/plugins/entry.ts @@ -66,8 +66,8 @@ export const pluginEntryWatch: (params: { const sourceEntries = await globTestSourceEntries(); config.entry = { - ...sourceEntries, ...setupFiles, + ...sourceEntries, }; } }); diff --git a/packages/core/src/core/plugins/mockRuntime.ts b/packages/core/src/core/plugins/mockRuntime.ts new file mode 100644 index 00000000..fcdc7cbd --- /dev/null +++ b/packages/core/src/core/plugins/mockRuntime.ts @@ -0,0 +1,90 @@ +import type { RsbuildPlugin, Rspack } from '@rsbuild/core'; + +class MockRuntimeRspackPlugin { + apply(compiler: Rspack.Compiler) { + const { RuntimeModule } = compiler.webpack; + class RetestImportRuntimeModule extends RuntimeModule { + constructor() { + super('rstest runtime'); + } + + override generate() { + // Rstest runtime code should be prefixed with `rstest_` to avoid conflicts with other runtimes. + return ` +if (typeof __webpack_require__ === 'undefined') { + return; +} + +const originalRequire = __webpack_require__; +__webpack_require__ = function(...args) { + try { + return originalRequire(...args); + } catch (e) { + const errMsg = e.message ?? e.toString(); + if (errMsg.includes('__webpack_modules__[moduleId] is not a function')) { + throw new Error(\`Cannot find module '\${args[0]}'\`) + } + throw e; + } +}; + +Object.keys(originalRequire).forEach(key => { + __webpack_require__[key] = originalRequire[key]; +}); + +__webpack_require__.rstest_original_modules = {}; + +// TODO: Remove "reset_modules" in next Rspack version. +__webpack_require__.rstest_reset_modules = __webpack_require__.reset_modules = () => { + __webpack_module_cache__ = {}; +} + +// TODO: Remove "unmock" in next Rspack version. +__webpack_require__.rstest_unmock = __webpack_require__.unmock = (id) => { + delete __webpack_module_cache__[id] +} + +// TODO: Remove "require_actual" and "import_actual" in next Rspack version. +__webpack_require__.rstest_require_actual = __webpack_require__.rstest_import_actual = __webpack_require__.require_actual = __webpack_require__.import_actual = (id) => { + const originalModule = __webpack_require__.rstest_original_modules[id]; + // Use fallback module if the module is not mocked. + const fallbackMod = __webpack_require__(id); + return originalModule ? originalModule : fallbackMod; +} + +// TODO: Remove "set_mock" in next Rspack version. +__webpack_require__.rstest_set_mock = __webpack_require__.set_mock = (id, modFactory) => { + try { + __webpack_require__.rstest_original_modules[id] = __webpack_require__(id); + } catch { + // TODO: non-resolved module + } + if (typeof modFactory === 'string' || typeof modFactory === 'number') { + __webpack_module_cache__[id] = { exports: __webpack_require__(modFactory) }; + } else if (typeof modFactory === 'function') { + __webpack_module_cache__[id] = { exports: modFactory() }; + } +}; +`; + } + } + + compiler.hooks.thisCompilation.tap('CustomPlugin', (compilation) => { + compilation.hooks.additionalTreeRuntimeRequirements.tap( + 'CustomPlugin', + (chunk) => { + compilation.addRuntimeModule(chunk, new RetestImportRuntimeModule()); + }, + ); + }); + } +} + +export const pluginMockRuntime: RsbuildPlugin = { + name: 'rstest:mock-runtime', + setup: (api) => { + api.modifyRspackConfig(async (config) => { + config.plugins!.push(new MockRuntimeRspackPlugin()); + }); + }, +}; diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index 25855933..d98dc05c 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -19,6 +19,7 @@ import { } from '../utils'; import { pluginEntryWatch } from './plugins/entry'; import { pluginIgnoreResolveError } from './plugins/ignoreResolveError'; +import { pluginMockRuntime } from './plugins/mockRuntime'; const isMultiCompiler = < C extends Rspack.Compiler = Rspack.Compiler, @@ -50,7 +51,7 @@ const autoExternalNodeModules: ( callback( undefined, externalPath, - dependencyType === 'commonjs' ? 'commonjs' : 'module-import', + dependencyType === 'commonjs' ? 'commonjs' : 'import', ); }; @@ -173,9 +174,9 @@ export const prepareRsbuild = async ( config.plugins.push( new rspack.experiments.RstestPlugin({ injectModulePathName: true, - hoistMockModule: true, importMetaPathName: true, - manualMockRoot: context.rootPath, + hoistMockModule: true, + manualMockRoot: path.resolve(context.rootPath, '__mocks__'), }), ); @@ -206,11 +207,14 @@ export const prepareRsbuild = async ( ...(config.module.parser.javascript || {}), }; + config.resolve ??= {}; + config.resolve.extensions ??= []; + config.resolve.extensions.push('.cjs'); + if ( testEnvironment === 'node' && (getNodeVersion()[0] || 0) < 20 ) { - config.resolve ??= {}; // skip `module` field in Node.js 18 and below. // ESM module resolved by module field is not always a native ESM module config.resolve.mainFields = config.resolve.mainFields?.filter( @@ -231,6 +235,7 @@ export const prepareRsbuild = async ( }, plugins: [ pluginIgnoreResolveError, + pluginMockRuntime, pluginEntryWatch({ globTestSourceEntries, setupFiles, diff --git a/packages/core/src/runtime/api/index.ts b/packages/core/src/runtime/api/index.ts index f56da418..f62a3946 100644 --- a/packages/core/src/runtime/api/index.ts +++ b/packages/core/src/runtime/api/index.ts @@ -39,6 +39,7 @@ export const createRstestRuntime = ( }); const rstest = createRstestUtilities(); + return { runner, api: { diff --git a/packages/core/src/runtime/api/utilities.ts b/packages/core/src/runtime/api/utilities.ts index ab643ec8..4a9c133d 100644 --- a/packages/core/src/runtime/api/utilities.ts +++ b/packages/core/src/runtime/api/utilities.ts @@ -43,30 +43,41 @@ export const createRstestUtilities: () => RstestUtilities = () => { return rstest; }, mock: () => { - // TODO + // The actual implementation is managed by the built-in Rstest plugin. + }, + mockRequire: () => { + // The actual implementation is managed by the built-in Rstest plugin. }, doMock: () => { - // TODO + // The actual implementation is managed by the built-in Rstest plugin. + }, + doMockRequire: () => { + // The actual implementation is managed by the built-in Rstest plugin. }, - unMock: () => { - // TODO + unmock: () => { + // The actual implementation is managed by the built-in Rstest plugin. }, - doUnMock: () => { - // TODO + doUnmock: () => { + // The actual implementation is managed by the built-in Rstest plugin. }, importMock: async () => { + // The actual implementation is managed by the built-in Rstest plugin. + return {} as any; + }, + requireMock: () => { + // The actual implementation is managed by the built-in Rstest plugin. return {} as any; }, importActual: async () => { - // The real implementation is handled by Rstest built-in plugin. + // The actual implementation is managed by the built-in Rstest plugin. return {} as any; }, requireActual: () => { - // The real implementation is handled by Rstest built-in plugin. + // The actual implementation is managed by the built-in Rstest plugin. return {} as any; }, resetModules: () => { - // TODO + // The actual implementation is managed by the built-in Rstest plugin. return rstest; }, diff --git a/packages/core/src/runtime/worker/loadModule.ts b/packages/core/src/runtime/worker/loadModule.ts index 722b0eb4..d7fe0a53 100644 --- a/packages/core/src/runtime/worker/loadModule.ts +++ b/packages/core/src/runtime/worker/loadModule.ts @@ -68,6 +68,12 @@ const defineRstestDynamicImport = const modulePath = typeof resolvedPath === 'string' ? resolvedPath : resolvedPath.pathname; + // Rstest importAttributes is used internally to distinguish `importActual` and normal imports, + // and should not be passed to Node.js side, otherwise it will cause ERR_IMPORT_ATTRIBUTE_UNSUPPORTED error. + if (importAttributes?.with?.rstest) { + delete importAttributes.with.rstest; + } + const importedModule = await import( modulePath, importAttributes as ImportCallOptions diff --git a/packages/core/src/types/mock.ts b/packages/core/src/types/mock.ts index 749c82e1..3099f2ef 100644 --- a/packages/core/src/types/mock.ts +++ b/packages/core/src/types/mock.ts @@ -192,49 +192,62 @@ export type RstestUtilities = { restoreAllMocks: () => RstestUtilities; /** - * @todo * Mock a module */ mock: (moduleName: string, moduleFactory?: () => T) => void; /** - * @todo + * Mock a module + */ + mockRequire: ( + moduleName: string, + moduleFactory?: () => T, + ) => void; + + /** * Mock a module, not hoisted. */ doMock: (moduleName: string, moduleFactory?: () => T) => void; /** - * @todo + * Mock a module, not hoisted. + */ + doMockRequire: ( + moduleName: string, + moduleFactory?: () => T, + ) => void; + + /** * Removes module from the mocked registry. */ - unMock: (path: string) => void; + unmock: (path: string) => void; /** - * @todo * Removes module from the mocked registry, not hoisted. */ - doUnMock: (path: string) => void; + doUnmock: (path: string) => void; /** - * @todo * Imports a module with all of its properties (including nested properties) mocked. */ importMock: >(path: string) => Promise; /** - * @todo + * Imports a module with all of its properties (including nested properties) mocked. + */ + requireMock: >(path: string) => T; + + /** * Import and return the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. */ importActual: >(path: string) => Promise; /** - * @todo * Require and return the actual module instead of a mock, bypassing all checks on whether the module should receive a mock implementation or not. */ requireActual: >(path: string) => T; /** - * @todo * Resets modules registry by clearing the cache of all modules. */ resetModules: () => RstestUtilities; diff --git a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap index 1fbb7303..f6b25042 100644 --- a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap +++ b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap @@ -416,13 +416,14 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] = }, }, IgnoreModuleNotFoundErrorPlugin {}, + MockRuntimeRspackPlugin {}, RstestPlugin { "_args": [ { "hoistMockModule": true, "importMetaPathName": true, "injectModulePathName": true, - "manualMockRoot": undefined, + "manualMockRoot": "/__mocks__", }, ], "affectedHooks": undefined, @@ -440,6 +441,7 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] = ".js", ".jsx", ".json", + ".cjs", ], }, "target": "node", @@ -868,13 +870,14 @@ exports[`prepareRsbuild > should generate rspack config correctly (node) 1`] = ` }, }, IgnoreModuleNotFoundErrorPlugin {}, + MockRuntimeRspackPlugin {}, RstestPlugin { "_args": [ { "hoistMockModule": true, "importMetaPathName": true, "injectModulePathName": true, - "manualMockRoot": undefined, + "manualMockRoot": "/__mocks__", }, ], "affectedHooks": undefined, @@ -892,6 +895,7 @@ exports[`prepareRsbuild > should generate rspack config correctly (node) 1`] = ` ".js", ".jsx", ".json", + ".cjs", ], }, "target": "node", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5551c51d..ee3cdd0a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -219,12 +219,21 @@ importers: '@types/jest-image-snapshot': specifier: ^6.4.0 version: 6.4.0 + axios: + specifier: ^1.9.0 + version: 1.10.0 jest-image-snapshot: specifier: ^6.5.1 version: 6.5.1 + memfs: + specifier: ^4.17.2 + version: 4.17.2 pathe: specifier: ^2.0.3 version: 2.0.3 + redux: + specifier: ^5.0.1 + version: 5.0.1 strip-ansi: specifier: ^7.1.0 version: 7.1.0 @@ -687,6 +696,24 @@ packages: '@jridgewell/trace-mapping@0.3.27': resolution: {integrity: sha512-VO95AxtSFMelbg3ouljAYnfvTEwSWVt/2YLf+U5Ejd8iT5mXE2Sa/1LGyvySMne2CGsepGLI7KpF3EzE3Aq9Mg==} + '@jsonjoy.com/base64@1.1.2': + resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/json-pack@1.2.0': + resolution: {integrity: sha512-io1zEbbYcElht3tdlqEOFxZ0dMTYrHz9iMf0gqn1pPjZFTCgM5R4R5IMA20Chb2UPYYsxjzs8CgZ7Nb5n2K2rA==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + + '@jsonjoy.com/util@1.6.0': + resolution: {integrity: sha512-sw/RMbehRhN68WRtcKCpQOPfnH6lLP4GJfqzi3iYej8tnzpZUDr6UkZYJjcjjC0FWEJOJbyM3PTIwxucUmDG2A==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -2187,6 +2214,10 @@ packages: resolution: {integrity: sha512-3gKm/gCSUipeLsRYZbbdA1BD83lBoWUkZ7G9VFrhWPAU76KwYo5KR8V28bpoPm/ygy0x5/GCbpRQdY7VLYCoIg==} hasBin: true + hyperdyperid@1.2.0: + resolution: {integrity: sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==} + engines: {node: '>=10.18'} + iconv-lite@0.4.24: resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} engines: {node: '>=0.10.0'} @@ -2559,6 +2590,10 @@ packages: medium-zoom@1.1.0: resolution: {integrity: sha512-ewyDsp7k4InCUp3jRmwHBRFGyjBimKps/AJLjRSox+2q/2H4p/PNpQf+pwONWlJiOudkBXtbdmVbFjqyybfTmQ==} + memfs@4.17.2: + resolution: {integrity: sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==} + engines: {node: '>= 4.0.0'} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3044,6 +3079,9 @@ packages: reduce-configs@1.1.0: resolution: {integrity: sha512-DQxy6liNadHfrLahZR7lMdc227NYVaQZhY5FMsxLEjX8X0SCuH+ESHSLCoz2yDZFq1/CLMDOAHdsEHwOEXKtvg==} + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + regenerator-runtime@0.14.1: resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} @@ -3541,6 +3579,12 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} + thingies@1.21.0: + resolution: {integrity: sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==} + engines: {node: '>=10.18'} + peerDependencies: + tslib: ^2 + tinyexec@1.0.1: resolution: {integrity: sha512-5uC6DDlmeqiOwCPmK9jMSdOuZTh8bU39Ys6yidB+UTt5hfZUPGAypSgFRiEp+jbi9qH40BLDvy85jIU88wKSqw==} @@ -3598,6 +3642,12 @@ packages: resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} engines: {node: '>=18'} + tree-dump@1.0.3: + resolution: {integrity: sha512-il+Cv80yVHFBwokQSfd4bldvr1Md951DpgAGfmhydt04L+YzHgubm2tQ7zueWDcGENKHq0ZvGFR/hjvNXilHEg==} + engines: {node: '>=10.0'} + peerDependencies: + tslib: '2' + tree-kill@1.2.2: resolution: {integrity: sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==} hasBin: true @@ -4165,6 +4215,22 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 + '@jsonjoy.com/base64@1.1.2(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + + '@jsonjoy.com/json-pack@1.2.0(tslib@2.8.1)': + dependencies: + '@jsonjoy.com/base64': 1.1.2(tslib@2.8.1) + '@jsonjoy.com/util': 1.6.0(tslib@2.8.1) + hyperdyperid: 1.2.0 + thingies: 1.21.0(tslib@2.8.1) + tslib: 2.8.1 + + '@jsonjoy.com/util@1.6.0(tslib@2.8.1)': + dependencies: + tslib: 2.8.1 + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.26.9 @@ -6002,6 +6068,8 @@ snapshots: human-id@4.1.1: {} + hyperdyperid@1.2.0: {} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 @@ -6451,6 +6519,13 @@ snapshots: medium-zoom@1.1.0: {} + memfs@4.17.2: + dependencies: + '@jsonjoy.com/json-pack': 1.2.0(tslib@2.8.1) + '@jsonjoy.com/util': 1.6.0(tslib@2.8.1) + tree-dump: 1.0.3(tslib@2.8.1) + tslib: 2.8.1 + merge2@1.4.1: {} micromark-core-commonmark@2.0.3: @@ -7107,6 +7182,8 @@ snapshots: reduce-configs@1.1.0: {} + redux@5.0.1: {} + regenerator-runtime@0.14.1: {} regex-recursion@6.0.2: @@ -7626,6 +7703,10 @@ snapshots: term-size@2.2.1: {} + thingies@1.21.0(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tinyexec@1.0.1: {} tinyglobby@0.2.14: @@ -7669,6 +7750,10 @@ snapshots: dependencies: punycode: 2.3.1 + tree-dump@1.0.3(tslib@2.8.1): + dependencies: + tslib: 2.8.1 + tree-kill@1.2.2: {} trim-lines@3.0.1: {} diff --git a/tests/__mocks__/axios.ts b/tests/__mocks__/axios.ts new file mode 100644 index 00000000..5a6be39d --- /dev/null +++ b/tests/__mocks__/axios.ts @@ -0,0 +1,6 @@ +import { rs } from '@rstest/core'; + +export default { + get: rs.fn(), + mocked: 'axios_mocked', +}; diff --git a/tests/__mocks__/fs.js b/tests/__mocks__/fs.js new file mode 100644 index 00000000..abd6f2b2 --- /dev/null +++ b/tests/__mocks__/fs.js @@ -0,0 +1,6 @@ +// we can also use `import`, but then +// every export should be explicitly defined + +const { fs } = require('memfs'); + +module.exports = fs; diff --git a/tests/__mocks__/redux.ts b/tests/__mocks__/redux.ts new file mode 100644 index 00000000..e2986010 --- /dev/null +++ b/tests/__mocks__/redux.ts @@ -0,0 +1,6 @@ +import { rs } from '@rstest/core'; + +export default { + isAction: rs.fn(), + mocked: 'redux_yes', +}; diff --git a/tests/mock/fixtures/unmock/noUnmock.test.ts b/tests/mock/fixtures/unmock/noUnmock.test.ts new file mode 100644 index 00000000..0166d2ca --- /dev/null +++ b/tests/mock/fixtures/unmock/noUnmock.test.ts @@ -0,0 +1,6 @@ +import { randomFill } from 'node:crypto'; +import { expect, it } from '@rstest/core'; + +it('should run setup file correctly', () => { + expect(randomFill).toBe('mocked_randomFill'); +}); diff --git a/tests/mock/fixtures/unmock/rstest.config.ts b/tests/mock/fixtures/unmock/rstest.config.ts new file mode 100644 index 00000000..756c1642 --- /dev/null +++ b/tests/mock/fixtures/unmock/rstest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + passWithNoTests: true, + setupFiles: ['./rstest.setup.ts'], + exclude: ['**/node_modules/**', '**/dist/**'], +}); diff --git a/tests/mock/fixtures/unmock/rstest.setup.ts b/tests/mock/fixtures/unmock/rstest.setup.ts new file mode 100644 index 00000000..1e7706e1 --- /dev/null +++ b/tests/mock/fixtures/unmock/rstest.setup.ts @@ -0,0 +1,9 @@ +import { rs } from '@rstest/core'; + +process.env.NODE_ENV = 'rstest:production'; + +rs.mock('node:crypto', () => { + return { + randomFill: 'mocked_randomFill', + }; +}); diff --git a/tests/mock/fixtures/unmock/unmock.test.ts b/tests/mock/fixtures/unmock/unmock.test.ts new file mode 100644 index 00000000..a313ba47 --- /dev/null +++ b/tests/mock/fixtures/unmock/unmock.test.ts @@ -0,0 +1,8 @@ +import { randomFill } from 'node:crypto'; +import { expect, it, rs } from '@rstest/core'; + +rs.unmock('node:crypto'); + +it('should run setup file correctly2', () => { + expect(typeof randomFill === 'function').toBe(true); +}); diff --git a/tests/mock/src/increment.ts b/tests/mock/src/increment.ts new file mode 100644 index 00000000..cb92242e --- /dev/null +++ b/tests/mock/src/increment.ts @@ -0,0 +1,3 @@ +export function increment(num: number) { + return num + 1; +} diff --git a/tests/mock/src/mockedDependency.ts b/tests/mock/src/mockedDependency.ts new file mode 100644 index 00000000..e22aff59 --- /dev/null +++ b/tests/mock/src/mockedDependency.ts @@ -0,0 +1,3 @@ +export function helloWorld(): void { + throw new Error('not implemented'); +} diff --git a/tests/mock/src/readSomeFile.ts b/tests/mock/src/readSomeFile.ts new file mode 100644 index 00000000..8a2f737a --- /dev/null +++ b/tests/mock/src/readSomeFile.ts @@ -0,0 +1,5 @@ +import { readFileSync } from 'node:fs'; + +export function readSomeFile(path: string) { + return readFileSync(path, 'utf-8'); +} diff --git a/tests/mock/tests/doMock.test.ts b/tests/mock/tests/doMock.test.ts new file mode 100644 index 00000000..c2c50000 --- /dev/null +++ b/tests/mock/tests/doMock.test.ts @@ -0,0 +1,58 @@ +import { expect, rs, test } from '@rstest/core'; + +test('doMock works', async () => { + const { increment: incrementWith1 } = await import('../src/increment'); + expect(incrementWith1(1)).toBe(2); + + rs.doMock('../src/increment', () => ({ + increment: (num: number) => num + 10, + })); + + const { increment: incrementWith10 } = await import('../src/increment'); + expect(incrementWith10(1)).toBe(11); +}); + +test('the second doMock can override the first doMock', async () => { + rs.doMock('../src/increment', () => ({ + increment: (num: number) => num + 10, + })); + + const { increment: incrementWith1 } = await import('../src/increment'); + + expect(incrementWith1(1)).toBe(11); + + rs.doMock('../src/increment', () => ({ + increment: (num: number) => num + 20, + })); + + const { increment: incrementWith20 } = await import('../src/increment'); + + expect(incrementWith20(1)).toBe(21); +}); + +test('the third doMock can override the second doMock', async () => { + const sleep = (ms: number) => + new Promise((resolve) => setTimeout(resolve, ms)); + + rs.doMock('../src/increment', async () => { + await sleep(500); + return { + increment: (num: number) => num + 100, + }; + }); + + const { increment: incrementWith1 } = await import('../src/increment'); + + expect(incrementWith1(1)).toBe(101); + + rs.doMock('../src/increment', async () => { + await sleep(500); + return { + increment: (num: number) => num + 200, + }; + }); + + const { increment: incrementWith20 } = await import('../src/increment'); + + expect(incrementWith20(1)).toBe(201); +}); diff --git a/tests/mock/tests/doMockRequire.test.ts b/tests/mock/tests/doMockRequire.test.ts new file mode 100644 index 00000000..f17feee4 --- /dev/null +++ b/tests/mock/tests/doMockRequire.test.ts @@ -0,0 +1,32 @@ +import { expect, rs, test } from '@rstest/core'; + +test('doMockRequire works', () => { + const { increment: incrementWith1 } = require('../src/increment'); + expect(incrementWith1(1)).toBe(2); + + rs.doMockRequire('../src/increment', () => ({ + increment: (num: number) => num + 10, + })); + + const { increment: incrementWith10 } = require('../src/increment'); + + expect(incrementWith10(1)).toBe(11); +}); + +test('the second doMockRequire can override the first doMockRequire', () => { + rs.doMockRequire('../src/increment', () => ({ + increment: (num: number) => num + 10, + })); + + const { increment: incrementWith1 } = require('../src/increment'); + + expect(incrementWith1(1)).toBe(11); + + rs.doMockRequire('../src/increment', () => ({ + increment: (num: number) => num + 20, + })); + + const { increment: incrementWith20 } = require('../src/increment'); + + expect(incrementWith20(1)).toBe(21); +}); diff --git a/tests/mock/tests/doUnmock.test.ts b/tests/mock/tests/doUnmock.test.ts new file mode 100644 index 00000000..9d3cd5ce --- /dev/null +++ b/tests/mock/tests/doUnmock.test.ts @@ -0,0 +1,29 @@ +import { beforeAll, expect, rs, test } from '@rstest/core'; +import { isAction } from 'redux'; + +beforeAll(() => { + rs.doMock('redux', () => ({ + isAction: { + state: 1, + }, + })); +}); + +test('first import', async () => { + const redux = await import('redux'); + + // doMock is not hoisted. + expect(typeof isAction).toBe('function'); + // @ts-expect-error + expect(redux.isAction.state).toBe(1); + // @ts-expect-error + redux.isAction.state = 2; + // @ts-expect-error + expect(redux.isAction.state).toBe(2); +}); + +test('second import should have been re-mocked', async () => { + rs.doUnmock('redux'); + const redux = await import('redux'); + expect(typeof redux.isAction).toBe('function'); +}); diff --git a/tests/mock/tests/external.test.ts b/tests/mock/tests/external.test.ts deleted file mode 100644 index f586915b..00000000 --- a/tests/mock/tests/external.test.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { expect, it, rstest } from '@rstest/core'; - -rstest.mock('picocolors', () => { - return { - sayHi: () => 'hi', - }; -}); - -// TODO -it.todo('should mock external module correctly', async () => { - // @ts-expect-error - const { sayHi } = await import('picocolors'); - - expect(sayHi?.()).toBe('hi'); -}); diff --git a/tests/mock/tests/importActual.test.ts b/tests/mock/tests/importActual.test.ts new file mode 100644 index 00000000..032b1db2 --- /dev/null +++ b/tests/mock/tests/importActual.test.ts @@ -0,0 +1,20 @@ +import { expect, it, rs } from '@rstest/core'; +import axios from 'axios'; + +rs.mock('axios'); + +it('mocked axios (axios is externalized)', async () => { + await axios.get('string'); + expect(axios.get).toHaveBeenCalledWith('string'); + // @ts-expect-error + expect(axios.mocked).toBe('axios_mocked'); + expect(axios.post).toBeUndefined(); +}); + +it('use `importActual` to import actual axios', async () => { + const ax = await rs.importActual('axios'); + expect(rs.isMockFunction(ax.get)).toBe(false); + // @ts-expect-error + expect(ax.mocked).toBeUndefined(); + expect(typeof ax.AxiosHeaders).toBe('function'); +}); diff --git a/tests/mock/tests/index.test.ts b/tests/mock/tests/index.test.ts deleted file mode 100644 index a192a678..00000000 --- a/tests/mock/tests/index.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { expect, it, rstest } from '@rstest/core'; - -rstest.mock('../src/b', () => { - return { - b: 3, - }; -}); - -// TODO -it.todo('should mock relative path module correctly', async () => { - const { b } = await import('../src/index'); - expect(b).toBe(3); -}); diff --git a/tests/mock/tests/loadMock.test.ts b/tests/mock/tests/loadMock.test.ts new file mode 100644 index 00000000..60cbc46f --- /dev/null +++ b/tests/mock/tests/loadMock.test.ts @@ -0,0 +1,23 @@ +import { expect, rs, test } from '@rstest/core'; +import * as redux from 'redux'; + +test('importMock works', async () => { + const { default: rx } = await rs.importMock('redux'); + await rx.isAction('string'); + expect(rx.isAction).toHaveBeenCalledWith('string'); +}); + +test('actual redux is not mocked (ESM)', async () => { + expect(rs.isMockFunction(redux.isAction)).toBe(false); +}); + +test('requireMock works', async () => { + const rx = rs.requireMock('redux').default; + await rx.isAction('string'); + expect(rx.isAction).toHaveBeenCalledWith('string'); +}); + +test('actual redux is not mocked (CJS)', async () => { + const rx = require('redux'); + expect(rs.isMockFunction(rx.isAction)).toBe(false); +}); diff --git a/tests/mock/tests/mock.test.ts b/tests/mock/tests/mock.test.ts new file mode 100644 index 00000000..c894c9f8 --- /dev/null +++ b/tests/mock/tests/mock.test.ts @@ -0,0 +1,33 @@ +import { expect, it, rs } from '@rstest/core'; + +// TODO: static import with mockFactory, any other imported module in mockFactory +// will throw an reference error as it hoisted to the top of the module. +// import r from 'redux'; + +// manual mock +rs.mock('redux'); + +it('mocked redux', async () => { + const redux = (await import('redux')).default; + redux.isAction('string'); + expect(redux.isAction).toHaveBeenCalledWith('string'); + // @ts-ignore + expect(redux.mocked).toBe('redux_yes'); +}); + +// mock factory +rs.mock('axios', async () => { + const originalAxios = await rs.importActual('axios'); + return { + ...originalAxios, + post: rs.fn(), + }; +}); + +it('mocked axios', async () => { + const axios = await import('axios'); + // @ts-expect-error + expect(rs.isMockFunction(axios.post)).toBe(true); + // @ts-expect-error + expect(rs.isMockFunction(axios.get)).toBe(false); +}); diff --git a/tests/mock/tests/mockNodeProtocol.test.ts b/tests/mock/tests/mockNodeProtocol.test.ts new file mode 100644 index 00000000..c57a047c --- /dev/null +++ b/tests/mock/tests/mockNodeProtocol.test.ts @@ -0,0 +1,13 @@ +import { expect, it, rs } from '@rstest/core'; +import { fs } from 'memfs'; +import { readSomeFile } from '../src/readSomeFile'; + +rs.mock('node:fs'); + +it('should return correct text', () => { + const path = '/hello-world.txt'; + fs.writeFileSync(path, 'hello world'); + + const text = readSomeFile(path); + expect(text).toBe('hello world'); +}); diff --git a/tests/mock/tests/mockNonResolved.test.ts b/tests/mock/tests/mockNonResolved.test.ts new file mode 100644 index 00000000..919d1889 --- /dev/null +++ b/tests/mock/tests/mockNonResolved.test.ts @@ -0,0 +1,26 @@ +import { afterEach, beforeEach, expect, rs, test } from '@rstest/core'; + +beforeEach(() => { + rs.doMock('/data', () => ({ + data: { + state: 'STARTED', + }, + })); +}); + +afterEach(() => { + rs.doUnmock('/data'); +}); + +test('first import', async () => { + // @ts-expect-error I know this + const { data } = await import('/data'); + data.state = 'STOPPED'; + expect(data.state).toBe('STOPPED'); +}); + +test('second import should have been re-mocked', async () => { + // @ts-expect-error + const { data } = await import('/data'); + expect(data.state).toBe('STARTED'); +}); diff --git a/tests/mock/tests/mockRequire.test.ts b/tests/mock/tests/mockRequire.test.ts new file mode 100644 index 00000000..97095361 --- /dev/null +++ b/tests/mock/tests/mockRequire.test.ts @@ -0,0 +1,11 @@ +import { expect, it, rs } from '@rstest/core'; + +rs.mockRequire('redux'); + +it('mocked redux', () => { + const redux = require('redux').default; + redux.isAction('string'); + expect(redux.isAction).toHaveBeenCalledWith('string'); + // @ts-ignore + expect(redux.mocked).toBe('redux_yes'); +}); diff --git a/tests/mock/tests/reduxMocked.test.ts b/tests/mock/tests/reduxMocked.test.ts new file mode 100644 index 00000000..cebd62c0 --- /dev/null +++ b/tests/mock/tests/reduxMocked.test.ts @@ -0,0 +1,25 @@ +import { expect, it, rs } from '@rstest/core'; +import redux from 'redux'; + +rs.mock('redux'); + +it('mocked redux', async () => { + await redux.isAction('string'); + expect(redux.isAction).toHaveBeenCalledWith('string'); + // @ts-ignore + expect(redux.mocked).toBe('redux_yes'); +}); + +it('importActual works', async () => { + const rx = await rs.importActual('redux'); + expect(rs.isMockFunction(rx.isAction)).toBe(false); + expect(typeof rx.applyMiddleware).toBe('function'); +}); + +it('requireActual and importActual works together', async () => { + const rxEsm = await rs.importActual('redux'); + const rxCjs = rs.requireActual('redux'); + expect(rs.isMockFunction(rxCjs.isAction)).toBe(false); + expect(typeof rxCjs.applyMiddleware).toBe('function'); + expect(rxEsm.compose).not.toBe(rxCjs.compose); +}); diff --git a/tests/mock/tests/requireActual.test.ts b/tests/mock/tests/requireActual.test.ts new file mode 100644 index 00000000..3487e601 --- /dev/null +++ b/tests/mock/tests/requireActual.test.ts @@ -0,0 +1,19 @@ +import { expect, it, rs } from '@rstest/core'; + +const axios = require('axios').default; + +rs.mockRequire('axios'); + +it('mocked axios (axios is externalized)', async () => { + await axios.get('string'); + expect(axios.get).toHaveBeenCalledWith('string'); + expect(axios.mocked).toBe('axios_mocked'); + expect(axios.post).toBeUndefined(); +}); + +it('use `requireActual` to require actual axios', async () => { + const originalAxios = await rs.requireActual('axios'); + expect(rs.isMockFunction(originalAxios.get)).toBe(false); + expect(originalAxios.mocked).toBeUndefined(); + expect(typeof originalAxios.AxiosHeaders).toBe('function'); +}); diff --git a/tests/mock/tests/resetModules.test.ts b/tests/mock/tests/resetModules.test.ts new file mode 100644 index 00000000..686d7394 --- /dev/null +++ b/tests/mock/tests/resetModules.test.ts @@ -0,0 +1,10 @@ +import { expect, rs, test } from '@rstest/core'; + +test('reset modules works', async () => { + const mod1 = await import('../src/b'); + rs.resetModules(); + const mod2 = await import('../src/b'); + const mod3 = await import('../src/b'); + expect(mod1).not.toBe(mod2); + expect(mod2).toBe(mod3); +}); diff --git a/tests/mock/tests/unmock.test.ts b/tests/mock/tests/unmock.test.ts new file mode 100644 index 00000000..b40802f6 --- /dev/null +++ b/tests/mock/tests/unmock.test.ts @@ -0,0 +1,22 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { it } from '@rstest/core'; +import { runRstestCli } from '../../scripts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +it('unmock works', async () => { + const { cli, expectExecSuccess } = await runRstestCli({ + command: 'rstest', + args: ['run'], + options: { + nodeOptions: { + cwd: join(__dirname, '../fixtures/unmock'), + }, + }, + }); + + await cli.exec; + await expectExecSuccess(); +}); diff --git a/tests/package.json b/tests/package.json index 393f5771..fef0aeca 100644 --- a/tests/package.json +++ b/tests/package.json @@ -12,10 +12,13 @@ "@rstest/core": "workspace:*", "@rstest/tsconfig": "workspace:*", "@types/jest-image-snapshot": "^6.4.0", + "axios": "^1.9.0", "jest-image-snapshot": "^6.5.1", "pathe": "^2.0.3", + "redux": "^5.0.1", "strip-ansi": "^7.1.0", "tinyexec": "^1.0.1", + "memfs": "^4.17.2", "typescript": "^5.8.3" } } diff --git a/tests/scripts/index.ts b/tests/scripts/index.ts index 07fa8775..1f3a0d9d 100644 --- a/tests/scripts/index.ts +++ b/tests/scripts/index.ts @@ -87,7 +87,7 @@ export async function runRstestCli({ if (exitCode !== 0) { const logs = cli.stdout.split('\n').filter(Boolean); throw new Error( - `Test failed with exit code ${exitCode}. Logs: ${logs.join('\n')}`, + `Test failed with exit code ${exitCode}. Logs:\n\n${logs.join('\n')}`, ); } }; diff --git a/tests/test-api/edgeCase.test.ts b/tests/test-api/edgeCase.test.ts index b5dafe10..861a6633 100644 --- a/tests/test-api/edgeCase.test.ts +++ b/tests/test-api/edgeCase.test.ts @@ -25,8 +25,7 @@ describe('Test Edge Cases', () => { expect(logs.find((log) => log.includes('Error: Symbol('))).toBeFalsy(); }); - // TODO: Throw user friendly error message in webpack runtime. - it.todo('test module not found', async () => { + it('test module not found', async () => { // Module not found errors should be silent at build time, and throw errors at runtime const { cli, expectExecSuccess } = await runRstestCli({ command: 'rstest', @@ -40,6 +39,7 @@ describe('Test Edge Cases', () => { await expectExecSuccess(); const logs = cli.stdout.split('\n').filter(Boolean); + expect(logs.find((log) => log.includes('Build error'))).toBeFalsy(); expect(logs.find((log) => log.includes('Module not found'))).toBeFalsy(); expect(logs.find((log) => log.includes('Tests 2 passed'))).toBeTruthy(); diff --git a/tests/tsconfig.json b/tests/tsconfig.json index 97474cf8..c0e9fd98 100644 --- a/tests/tsconfig.json +++ b/tests/tsconfig.json @@ -4,8 +4,7 @@ "outDir": "./dist", "baseUrl": "./", "rootDir": "./", - "declarationDir": "./dist-types", - "composite": true, + "declaration": false, "noEmit": true, "allowImportingTsExtensions": true }, From 9105246a35d9ff26bfa0fdd5b51f8bbd0d453640 Mon Sep 17 00:00:00 2001 From: 9aoy Date: Tue, 1 Jul 2025 15:12:54 +0800 Subject: [PATCH 02/10] fix: should run tests correctly when isolate false (#343) --- .../src/core/plugins/moduleCacheControl.ts | 69 +++++++++++++++++++ packages/core/src/core/rsbuild.ts | 6 ++ packages/core/src/pool/index.ts | 1 + packages/core/src/runtime/worker/index.ts | 27 +++++++- .../core/src/runtime/worker/loadModule.ts | 3 +- packages/core/src/types/worker.ts | 1 + .../core/__snapshots__/rsbuild.test.ts.snap | 12 ++++ packages/core/tests/core/rsbuild.test.ts | 1 + tests/singleton/fixtures/index1.test.ts | 20 ++++++ tests/singleton/fixtures/rstest.config.ts | 13 ++++ tests/singleton/fixtures/setup.ts | 8 ++- tests/singleton/index.test.ts | 17 +++++ 12 files changed, 172 insertions(+), 6 deletions(-) create mode 100644 packages/core/src/core/plugins/moduleCacheControl.ts create mode 100644 tests/singleton/fixtures/index1.test.ts diff --git a/packages/core/src/core/plugins/moduleCacheControl.ts b/packages/core/src/core/plugins/moduleCacheControl.ts new file mode 100644 index 00000000..28ced6d4 --- /dev/null +++ b/packages/core/src/core/plugins/moduleCacheControl.ts @@ -0,0 +1,69 @@ +import type { RsbuildPlugin, Rspack } from '@rsbuild/core'; + +class RstestCacheControlPlugin { + apply(compiler: Rspack.Compiler) { + const { RuntimeModule } = compiler.webpack; + class RetestCacheControlModule extends RuntimeModule { + constructor() { + super('rstest_cache_control'); + } + + override generate() { + return ` +global.setupIds = []; + +function __rstest_clean_core_cache__() { + if (typeof __webpack_require__ === 'undefined') { + return; + } + delete __webpack_module_cache__['@rstest/core']; + + global.setupIds.forEach((id) => { + delete __webpack_module_cache__[id]; + }); +} + +global.__rstest_clean_core_cache__ = __rstest_clean_core_cache__; +`; + } + } + + compiler.hooks.thisCompilation.tap('CustomPlugin', (compilation) => { + compilation.hooks.additionalTreeRuntimeRequirements.tap( + 'CustomPlugin', + (chunk) => { + compilation.addRuntimeModule(chunk, new RetestCacheControlModule()); + }, + ); + }); + } +} + +/** + * clean setup and rstest module cache manually + * + * This is used to ensure that the setup files and rstest core are re-executed in each test run + * + * By default, modules are isolated between different tests (powered by tinypool). + */ +export const pluginCacheControl: (setupFiles: string[]) => RsbuildPlugin = ( + setupFiles: string[], +) => ({ + name: 'rstest:cache-control', + setup: (api) => { + api.transform({ test: setupFiles }, ({ code }) => { + // register setup's moduleId + return { + code: ` + ${code} + if (global.setupIds && __webpack_module__.id) { + global.setupIds.push(__webpack_module__.id); +} + `, + }; + }); + api.modifyRspackConfig(async (config) => { + config.plugins!.push(new RstestCacheControlPlugin()); + }); + }, +}); diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index d98dc05c..0625d626 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -20,6 +20,7 @@ import { import { pluginEntryWatch } from './plugins/entry'; import { pluginIgnoreResolveError } from './plugins/ignoreResolveError'; import { pluginMockRuntime } from './plugins/mockRuntime'; +import { pluginCacheControl } from './plugins/moduleCacheControl'; const isMultiCompiler = < C extends Rspack.Compiler = Rspack.Compiler, @@ -113,6 +114,7 @@ export const prepareRsbuild = async ( const { command, normalizedConfig: { + isolate, name, plugins, resolve, @@ -247,6 +249,10 @@ export const prepareRsbuild = async ( }, }); + if (!isolate) { + rsbuildInstance.addPlugins([pluginCacheControl(Object.values(setupFiles))]); + } + return rsbuildInstance; }; diff --git a/packages/core/src/pool/index.ts b/packages/core/src/pool/index.ts index c1a01872..c7d00320 100644 --- a/packages/core/src/pool/index.ts +++ b/packages/core/src/pool/index.ts @@ -148,6 +148,7 @@ export const createPool = async ({ printConsoleTrace, disableConsoleIntercept, testEnvironment, + isolate, }; const rpcMethods = { diff --git a/packages/core/src/runtime/worker/index.ts b/packages/core/src/runtime/worker/index.ts index 8e50ea56..6b47d0b1 100644 --- a/packages/core/src/runtime/worker/index.ts +++ b/packages/core/src/runtime/worker/index.ts @@ -10,7 +10,7 @@ import './setup'; import { globalApis } from '../../utils/constants'; import { undoSerializableConfig } from '../../utils/helper'; import { formatTestError } from '../util'; -import { cacheableLoadModule } from './loadModule'; +import { loadModule } from './loadModule'; import { createForksRpcOptions, createRuntimeRpc } from './rpc'; import { RstestSnapshotEnvironment } from './snapshot'; @@ -140,6 +140,7 @@ const loadFiles = async ({ distPath, testPath, interopDefault, + isolate, }: { setupEntries: RunWorkerOptions['options']['setupEntries']; assetFiles: RunWorkerOptions['options']['assetFiles']; @@ -147,12 +148,27 @@ const loadFiles = async ({ distPath: string; testPath: string; interopDefault: boolean; + isolate: boolean; }): Promise => { + // clean rstest core cache manually + if (!isolate) { + await loadModule({ + codeContent: `if (global && typeof global.__rstest_clean_core_cache__ === 'function') { + global.__rstest_clean_core_cache__(); + }`, + distPath, + testPath, + rstestContext, + assetFiles, + interopDefault, + }); + } + // run setup files for (const { distPath, testPath } of setupEntries) { const setupCodeContent = assetFiles[distPath]!; - await cacheableLoadModule({ + await loadModule({ codeContent: setupCodeContent, distPath, testPath, @@ -162,7 +178,7 @@ const loadFiles = async ({ }); } - await cacheableLoadModule({ + await loadModule({ codeContent: assetFiles[distPath]!, distPath, testPath, @@ -186,6 +202,9 @@ const runInPool = async ( setupEntries, assetFiles, type, + context: { + runtimeConfig: { isolate }, + }, } = options; const cleanups: (() => MaybePromise)[] = []; @@ -209,6 +228,7 @@ const runInPool = async ( assetFiles, setupEntries, interopDefault, + isolate, }); const tests = await runner.collectTests(); return { @@ -251,6 +271,7 @@ const runInPool = async ( assetFiles, setupEntries, interopDefault, + isolate, }); const results = await runner.runTests( testPath, diff --git a/packages/core/src/runtime/worker/loadModule.ts b/packages/core/src/runtime/worker/loadModule.ts index d7fe0a53..75b0cc88 100644 --- a/packages/core/src/runtime/worker/loadModule.ts +++ b/packages/core/src/runtime/worker/loadModule.ts @@ -128,7 +128,8 @@ const defineRstestDynamicImport = return importedModule; }; -const loadModule = ({ +// setup and rstest module should not be cached +export const loadModule = ({ codeContent, distPath, testPath, diff --git a/packages/core/src/types/worker.ts b/packages/core/src/types/worker.ts index b5a75faf..0baed83c 100644 --- a/packages/core/src/types/worker.ts +++ b/packages/core/src/types/worker.ts @@ -44,6 +44,7 @@ export type RuntimeConfig = Pick< | 'printConsoleTrace' | 'disableConsoleIntercept' | 'testEnvironment' + | 'isolate' >; export type WorkerContext = { diff --git a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap index f6b25042..0a4f8986 100644 --- a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap +++ b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap @@ -344,6 +344,17 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] = }, ], }, + { + "use": [ + { + "loader": "/node_modules//@rsbuild/core/dist/transformLoader.mjs", + "options": { + "getEnvironment": [Function], + "id": "rsbuild-transform-1", + }, + }, + ], + }, ], }, "name": "test", @@ -417,6 +428,7 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] = }, IgnoreModuleNotFoundErrorPlugin {}, MockRuntimeRspackPlugin {}, + RstestCacheControlPlugin {}, RstestPlugin { "_args": [ { diff --git a/packages/core/tests/core/rsbuild.test.ts b/packages/core/tests/core/rsbuild.test.ts index 7d8fef61..aef7b63e 100644 --- a/packages/core/tests/core/rsbuild.test.ts +++ b/packages/core/tests/core/rsbuild.test.ts @@ -37,6 +37,7 @@ describe('prepareRsbuild', () => { output: {}, tools: {}, testEnvironment: 'node', + isolate: true, }, } as unknown as RstestContext, async () => ({}), diff --git a/tests/singleton/fixtures/index1.test.ts b/tests/singleton/fixtures/index1.test.ts new file mode 100644 index 00000000..6a87b692 --- /dev/null +++ b/tests/singleton/fixtures/index1.test.ts @@ -0,0 +1,20 @@ +import { expect, it } from '@rstest/core'; +import { getA } from './a'; + +it('should singleton A - 1', () => { + expect(getA()).toBe(process.env.A); +}); + +it('should singleton B - 2', async () => { + if (process.env.TestNoIsolate) { + const { getB } = await import('./b'); + expect(getB()).toBe(process.env.B); + } else { + expect(process.env.B).toBeUndefined(); + } +}); + +it('should singleton C', async () => { + const { getB } = await import('./b'); + expect(getB()).toBe(process.env.C); +}); diff --git a/tests/singleton/fixtures/rstest.config.ts b/tests/singleton/fixtures/rstest.config.ts index 4289fdbb..c9632d61 100644 --- a/tests/singleton/fixtures/rstest.config.ts +++ b/tests/singleton/fixtures/rstest.config.ts @@ -1,5 +1,18 @@ import { defineConfig } from '@rstest/core'; +const TestNoIsolate = process.env.TestNoIsolate === 'true'; + export default defineConfig({ setupFiles: ['./setup.ts'], + isolate: !TestNoIsolate, + source: { + define: { + 'process.env.TestNoIsolate': TestNoIsolate, + }, + }, + pool: { + type: 'forks', + maxWorkers: 1, + minWorkers: 1, + }, }); diff --git a/tests/singleton/fixtures/setup.ts b/tests/singleton/fixtures/setup.ts index 26aa1042..481e0a51 100644 --- a/tests/singleton/fixtures/setup.ts +++ b/tests/singleton/fixtures/setup.ts @@ -1,12 +1,16 @@ import { beforeAll } from '@rstest/core'; import { getA } from './a'; -beforeAll(async () => { +beforeAll(async (context) => { const A = getA(); process.env.A = A; const { getB } = await import('./b'); const B = getB(); - process.env.B = B; + if (context.filepath.includes('index.test')) { + process.env.B = B; + } else { + process.env.C = B; + } }); diff --git a/tests/singleton/index.test.ts b/tests/singleton/index.test.ts index 54f205af..fc6d23b3 100644 --- a/tests/singleton/index.test.ts +++ b/tests/singleton/index.test.ts @@ -21,4 +21,21 @@ describe('test singleton', () => { await expectExecSuccess(); }); + + it('should load singleton module correctly when TestNoIsolate is true', async () => { + const { expectExecSuccess } = await runRstestCli({ + command: 'rstest', + args: ['run', 'index.test.ts'], + options: { + nodeOptions: { + cwd: join(__dirname, 'fixtures'), + env: { + TestNoIsolate: 'true', + }, + }, + }, + }); + + await expectExecSuccess(); + }); }); From 57747a85d24735272bc226d7d51b9bf6fc5d8adb Mon Sep 17 00:00:00 2001 From: 9aoy Date: Tue, 1 Jul 2025 17:39:38 +0800 Subject: [PATCH 03/10] docs: add more configuration items (#345) --- website/docs/en/config/test/_meta.json | 9 +++- .../config/test/disableConsoleIntercept.mdx | 8 ++++ website/docs/en/config/test/isolate.mdx | 18 +++++++ website/docs/en/config/test/name.mdx | 6 +++ website/docs/en/config/test/onConsoleLog.mdx | 16 +++++++ website/docs/en/config/test/pool.mdx | 48 +++++++++++++++++++ .../docs/en/config/test/printConsoleTrace.mdx | 14 ++++++ website/docs/en/config/test/update.mdx | 22 +++++++++ website/docs/zh/config/test/_meta.json | 9 +++- .../config/test/disableConsoleIntercept.mdx | 8 ++++ website/docs/zh/config/test/isolate.mdx | 18 +++++++ website/docs/zh/config/test/name.mdx | 6 +++ website/docs/zh/config/test/onConsoleLog.mdx | 16 +++++++ website/docs/zh/config/test/pool.mdx | 48 +++++++++++++++++++ .../docs/zh/config/test/printConsoleTrace.mdx | 14 ++++++ website/docs/zh/config/test/update.mdx | 22 +++++++++ 16 files changed, 280 insertions(+), 2 deletions(-) create mode 100644 website/docs/en/config/test/disableConsoleIntercept.mdx create mode 100644 website/docs/en/config/test/isolate.mdx create mode 100644 website/docs/en/config/test/name.mdx create mode 100644 website/docs/en/config/test/onConsoleLog.mdx create mode 100644 website/docs/en/config/test/pool.mdx create mode 100644 website/docs/en/config/test/printConsoleTrace.mdx create mode 100644 website/docs/en/config/test/update.mdx create mode 100644 website/docs/zh/config/test/disableConsoleIntercept.mdx create mode 100644 website/docs/zh/config/test/isolate.mdx create mode 100644 website/docs/zh/config/test/name.mdx create mode 100644 website/docs/zh/config/test/onConsoleLog.mdx create mode 100644 website/docs/zh/config/test/pool.mdx create mode 100644 website/docs/zh/config/test/printConsoleTrace.mdx create mode 100644 website/docs/zh/config/test/update.mdx diff --git a/website/docs/en/config/test/_meta.json b/website/docs/en/config/test/_meta.json index 88d7e809..8a5c7c33 100644 --- a/website/docs/en/config/test/_meta.json +++ b/website/docs/en/config/test/_meta.json @@ -7,5 +7,12 @@ "testEnvironment", "retry", "root", - "passWithNoTests" + "name", + "isolate", + "pool", + "passWithNoTests", + "printConsoleTrace", + "onConsoleLog", + "disableConsoleIntercept", + "update" ] diff --git a/website/docs/en/config/test/disableConsoleIntercept.mdx b/website/docs/en/config/test/disableConsoleIntercept.mdx new file mode 100644 index 00000000..e5a07d1a --- /dev/null +++ b/website/docs/en/config/test/disableConsoleIntercept.mdx @@ -0,0 +1,8 @@ +# disableConsoleIntercept + +- **Type:** `boolean` +- **Default:** `false` + +Disable interception of console logs. By default, Rstest intercepts the console log, which will help track log sources. + +If you don't want Rstest to intercept console logs, you can set this configuration to `true`. It should be noted that when you disable interception of console logs, the `onConsoleLog` and `printConsoleTrace` configurations will not take effect. diff --git a/website/docs/en/config/test/isolate.mdx b/website/docs/en/config/test/isolate.mdx new file mode 100644 index 00000000..61cbf282 --- /dev/null +++ b/website/docs/en/config/test/isolate.mdx @@ -0,0 +1,18 @@ +# isolate + +- **Type:** `boolean` +- **Default:** `true` + +Run tests in an isolated environment. + +By default, Rstest runs each test in an isolated environment, which avoids the impact of some module side effects and helps improve the stability of the test. + +If your code has no side effects, turning off this option will help improve performance because module caches can be reused between different test files. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + isolate: false, +}); +``` diff --git a/website/docs/en/config/test/name.mdx b/website/docs/en/config/test/name.mdx new file mode 100644 index 00000000..0bb52828 --- /dev/null +++ b/website/docs/en/config/test/name.mdx @@ -0,0 +1,6 @@ +# name + +- **Type:** `string` +- **Default:** `rstest` + +The name of the test project. diff --git a/website/docs/en/config/test/onConsoleLog.mdx b/website/docs/en/config/test/onConsoleLog.mdx new file mode 100644 index 00000000..4376ed4e --- /dev/null +++ b/website/docs/en/config/test/onConsoleLog.mdx @@ -0,0 +1,16 @@ +# onConsoleLog + +- **Type:** `(content: string) => boolean | void` +- **Default:** `undefined` + +Custom handler for console log in tests, which is helpful for filtering out logs from third-party libraries. + +If you return `false`, Rstest will not print the log to the console. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + onConsoleLog: (log) => log.includes('test'), +}); +``` diff --git a/website/docs/en/config/test/pool.mdx b/website/docs/en/config/test/pool.mdx new file mode 100644 index 00000000..e892dcef --- /dev/null +++ b/website/docs/en/config/test/pool.mdx @@ -0,0 +1,48 @@ +--- +overviewHeaders: [] +--- + +# pool + +- **Type:** + +```ts +export type RstestPoolOptions = { + /** Pool used to run tests in. */ + type: 'fork'; + /** Maximum number or percentage of workers to run tests in. */ + maxWorkers?: number | string; + /** Minimum number or percentage of workers to run tests in. */ + minWorkers?: number | string; + /** Pass additional arguments to node process in the child processes. */ + execArgv?: string[]; +}; +``` + +- **Default:** + +```ts +const defaultPool = { + type: 'fork' + maxWorkers: available CPUs + minWorkers: available CPUs +} +``` + +Options of pool used to run tests in. + +## Example + +Run all tests inside a single child process. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + pool: { + type: 'forks', + maxWorkers: 1, + minWorkers: 1, + }, +}); +``` diff --git a/website/docs/en/config/test/printConsoleTrace.mdx b/website/docs/en/config/test/printConsoleTrace.mdx new file mode 100644 index 00000000..7bdaea78 --- /dev/null +++ b/website/docs/en/config/test/printConsoleTrace.mdx @@ -0,0 +1,14 @@ +# printConsoleTrace + +- **Type:** `boolean` +- **Default:** `false` + +Print console traces when calling any console method, which is helpful for debugging. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + printConsoleTrace: true, +}); +``` diff --git a/website/docs/en/config/test/update.mdx b/website/docs/en/config/test/update.mdx new file mode 100644 index 00000000..f75932cb --- /dev/null +++ b/website/docs/en/config/test/update.mdx @@ -0,0 +1,22 @@ +--- +overviewHeaders: [] +--- + +# update + +- **Type:** `boolean` +- **Default:** `false` + +Update snapshot. + +When `update` is enabled, Rstest will update all changed snapshots and delete obsolete ones. + +It should be noted that when you test in local, the newly added snapshot will be automatically updated regardless of whether the `update` configuration is enabled. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + update: true, +}); +``` diff --git a/website/docs/zh/config/test/_meta.json b/website/docs/zh/config/test/_meta.json index 88d7e809..8a5c7c33 100644 --- a/website/docs/zh/config/test/_meta.json +++ b/website/docs/zh/config/test/_meta.json @@ -7,5 +7,12 @@ "testEnvironment", "retry", "root", - "passWithNoTests" + "name", + "isolate", + "pool", + "passWithNoTests", + "printConsoleTrace", + "onConsoleLog", + "disableConsoleIntercept", + "update" ] diff --git a/website/docs/zh/config/test/disableConsoleIntercept.mdx b/website/docs/zh/config/test/disableConsoleIntercept.mdx new file mode 100644 index 00000000..51fbfe6e --- /dev/null +++ b/website/docs/zh/config/test/disableConsoleIntercept.mdx @@ -0,0 +1,8 @@ +# disableConsoleIntercept + +- **类型:** `boolean` +- **默认值:** `false` + +禁用 console 日志的拦截。默认情况下,Rstest 会对 console 日志做拦截,这样将有助于追踪日志来源。 + +如果你不想要 Rstest 对 console 日志做拦截,你可以将此配置设为 `true`。需要注意的是,当你禁用 console 日志的拦截时,`onConsoleLog` 和 `printConsoleTrace` 配置将不会生效。 diff --git a/website/docs/zh/config/test/isolate.mdx b/website/docs/zh/config/test/isolate.mdx new file mode 100644 index 00000000..92aa1daf --- /dev/null +++ b/website/docs/zh/config/test/isolate.mdx @@ -0,0 +1,18 @@ +# isolate + +- **类型:** `boolean` +- **默认值:** `true` + +是否运行每个测试在一个独立的环境。 + +默认情况下, Rstest 会运行每个测试在一个独立的环境,这会使其避免受到一些模块副作用的影响,从而有助于提升测试的稳定性。 + +如果你的代码没有副作用影响,关闭这个选项将有助于提升性能因为可以在不同的测试文件间复用模块缓存。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + isolate: false, +}); +``` diff --git a/website/docs/zh/config/test/name.mdx b/website/docs/zh/config/test/name.mdx new file mode 100644 index 00000000..a1844cc4 --- /dev/null +++ b/website/docs/zh/config/test/name.mdx @@ -0,0 +1,6 @@ +# name + +- **类型:** `string` +- **默认值:** `rstest` + +测试项目的名称。 diff --git a/website/docs/zh/config/test/onConsoleLog.mdx b/website/docs/zh/config/test/onConsoleLog.mdx new file mode 100644 index 00000000..483a15f4 --- /dev/null +++ b/website/docs/zh/config/test/onConsoleLog.mdx @@ -0,0 +1,16 @@ +# onConsoleLog + +- **类型:** `(content: string) => boolean | void` +- **默认值:** `undefined` + +自定义 console 日志的处理函数,这有助于过滤来自第三方库的日志。 + +如果你返回 `false`,Rstest 不会将此日志打印到控制台。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + onConsoleLog: (log) => log.includes('test'), +}); +``` diff --git a/website/docs/zh/config/test/pool.mdx b/website/docs/zh/config/test/pool.mdx new file mode 100644 index 00000000..69ee2da3 --- /dev/null +++ b/website/docs/zh/config/test/pool.mdx @@ -0,0 +1,48 @@ +--- +overviewHeaders: [] +--- + +# pool + +- **类型:** + +```ts +export type RstestPoolOptions = { + /** 用于运行测试的线程池 */ + type: 'fork'; + /** 最大运行的线程池的数量或百分比 */ + maxWorkers?: number | string; + /** 最小运行的线程池的数量或百分比 */ + minWorkers?: number | string; + /** 向子进程中的 node 进程传递附加参数。 */ + execArgv?: string[]; +}; +``` + +- **默认值:** + +```ts +const defaultPool = { + type: 'fork' + maxWorkers: available CPUs + minWorkers: available CPUs +} +``` + +用于运行测试的线程池的选项。 + +## 示例 + +在单个子进程中运行所有测试。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + pool: { + type: 'forks', + maxWorkers: 1, + minWorkers: 1, + }, +}); +``` diff --git a/website/docs/zh/config/test/printConsoleTrace.mdx b/website/docs/zh/config/test/printConsoleTrace.mdx new file mode 100644 index 00000000..a70ae152 --- /dev/null +++ b/website/docs/zh/config/test/printConsoleTrace.mdx @@ -0,0 +1,14 @@ +# printConsoleTrace + +- **类型:** `boolean` +- **默认值:** `false` + +console 方法调用时始终打印调用栈,这将有助于调试。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + printConsoleTrace: true, +}); +``` diff --git a/website/docs/zh/config/test/update.mdx b/website/docs/zh/config/test/update.mdx new file mode 100644 index 00000000..7a56e755 --- /dev/null +++ b/website/docs/zh/config/test/update.mdx @@ -0,0 +1,22 @@ +--- +overviewHeaders: [] +--- + +# update + +- **类型:** `boolean` +- **默认值:** `false` + +是否更新快照。 + +当开启更新时,Rstest 将在测试过程中自动更新所有变更的快照并删除过时的快照。 + +需要注意的是,当你在本地测试时,新增的 snapshot 会自动更新无论 `update` 配置是否开启。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + update: true, +}); +``` From 6373c4f067a3d9bd5f84891c2707f8722f5b6f36 Mon Sep 17 00:00:00 2001 From: 9aoy Date: Wed, 2 Jul 2025 10:54:23 +0800 Subject: [PATCH 04/10] docs: add mock configs and `MockInstance` API (#346) --- website/docs/en/api/rstest/_meta.json | 2 +- website/docs/en/api/rstest/mockFunctions.mdx | 30 ++ website/docs/en/api/rstest/mockInstance.mdx | 328 +++++++++++++++++++ website/docs/en/config/test/_meta.json | 5 +- website/docs/en/config/test/clearMocks.mdx | 16 + website/docs/en/config/test/resetMocks.mdx | 16 + website/docs/en/config/test/restoreMocks.mdx | 16 + website/docs/zh/api/rstest/_meta.json | 2 +- website/docs/zh/api/rstest/mockFunctions.mdx | 30 ++ website/docs/zh/api/rstest/mockInstance.mdx | 328 +++++++++++++++++++ website/docs/zh/config/test/_meta.json | 5 +- website/docs/zh/config/test/clearMocks.mdx | 16 + website/docs/zh/config/test/resetMocks.mdx | 16 + website/docs/zh/config/test/restoreMocks.mdx | 16 + 14 files changed, 822 insertions(+), 4 deletions(-) create mode 100644 website/docs/en/api/rstest/mockInstance.mdx create mode 100644 website/docs/en/config/test/clearMocks.mdx create mode 100644 website/docs/en/config/test/resetMocks.mdx create mode 100644 website/docs/en/config/test/restoreMocks.mdx create mode 100644 website/docs/zh/api/rstest/mockInstance.mdx create mode 100644 website/docs/zh/config/test/clearMocks.mdx create mode 100644 website/docs/zh/config/test/resetMocks.mdx create mode 100644 website/docs/zh/config/test/restoreMocks.mdx diff --git a/website/docs/en/api/rstest/_meta.json b/website/docs/en/api/rstest/_meta.json index f6eb67ef..f300f81e 100644 --- a/website/docs/en/api/rstest/_meta.json +++ b/website/docs/en/api/rstest/_meta.json @@ -1 +1 @@ -["mockFunctions", "fakeTimers"] +["mockFunctions", "mockInstance", "fakeTimers"] diff --git a/website/docs/en/api/rstest/mockFunctions.mdx b/website/docs/en/api/rstest/mockFunctions.mdx index e8af4915..a1dd9f12 100644 --- a/website/docs/en/api/rstest/mockFunctions.mdx +++ b/website/docs/en/api/rstest/mockFunctions.mdx @@ -8,6 +8,16 @@ Rstest provides some utility functions to help you mock functions. ## rstest.fn +- **Type:** + +```ts +export interface Mock extends MockInstance { + (...args: Parameters): ReturnType; +} + +export type MockFn = (fn?: T) => Mock; +``` + Creates a spy on a function. ```ts @@ -16,10 +26,22 @@ const sayHi = rstest.fn((name: string) => `hi ${name}`); const res = sayHi('bob'); expect(res).toBe('hi bob'); + +expect(sayHi).toHaveBeenCalledTimes(1); ``` ## rstest.spyOn +- **Type:** + +```ts +export type SpyFn = ( + obj: Record, + methodName: string, + accessType?: 'get' | 'set', +) => MockInstance; +``` + Creates a spy on a method of an object. ```ts @@ -37,16 +59,24 @@ expect(spy).toHaveBeenCalled(); ## rstest.isMockFunction +- **Type:** `(fn: any) => fn is MockInstance` + Determines if the given function is a mocked function. ## rstest.clearAllMocks +- **Type:** `() => Rstest` + Clears the `mock.calls`, `mock.instances`, `mock.contexts` and `mock.results` properties of all mocks. ## rstest.resetAllMocks +- **Type:** `() => Rstest` + Clears all mocks properties and reset each mock's implementation to its original. ## rstest.restoreAllMocks +- **Type:** `() => Rstest` + Reset all mocks and restore original descriptors of spied-on objects. diff --git a/website/docs/en/api/rstest/mockInstance.mdx b/website/docs/en/api/rstest/mockInstance.mdx new file mode 100644 index 00000000..a319b4c9 --- /dev/null +++ b/website/docs/en/api/rstest/mockInstance.mdx @@ -0,0 +1,328 @@ +--- +title: MockInstance +overviewHeaders: [2, 3] +--- + +# MockInstance + +`MockInstance` is the type of all mock/spy instances, providing a rich set of APIs for controlling and inspecting mocks. + +## getMockName + +- **Type:** `() => string` + +Returns the mock name string set by `.mockName()`. + +```ts +const fn = rstest.fn(); +fn.mockName('myMock'); +expect(fn.getMockName()).toBe('myMock'); +``` + +## mockName + +- **Type:** `(name: string) => MockInstance` + +Sets the mock name for this mock instance, useful for debugging and output. + +```ts +const fn = rstest.fn(); +fn.mockName('logger'); +``` + +## mockClear + +- **Type:** `() => MockInstance` + +Clears all information about every call (calls, instances, contexts, results, etc.). + +```ts +const fn = rstest.fn(); +fn(1); +fn.mockClear(); +expect(fn.mock.calls.length).toBe(0); +``` + +## mockReset + +- **Type:** `() => MockInstance` + +Clears all call information and resets the implementation to the initial state. + +```ts +const fn = rstest.fn().mockImplementation(() => 1); +fn.mockReset(); +// Implementation is reset +``` + +## mockRestore + +- **Type:** `() => MockInstance` + +Restores the original method of a spied object (only effective for spies). + +```ts +const obj = { foo: () => 1 }; +const spy = rstest.spyOn(obj, 'foo'); +spy.mockRestore(); +``` + +## getMockImplementation + +- **Type:** `() => Function | undefined` + +Returns the current mock implementation function, if any. + +```ts +const fn = rstest.fn(() => 123); +const impl = fn.getMockImplementation(); +``` + +## mockImplementation + +- **Type:** `(fn: Function) => MockInstance` + +Sets the implementation function for the mock. + +```ts +const fn = rstest.fn(); +fn.mockImplementation((a, b) => a + b); +``` + +## mockImplementationOnce + +- **Type:** `(fn: Function) => MockInstance` + +Sets the implementation function for the next call only. + +```ts +const fn = rstest.fn(); +fn.mockImplementationOnce(() => 1); +fn(); // returns 1 +fn(); // returns undefined +``` + +## withImplementation + +- **Type:** `(fn: Function, callback: () => any) => void | Promise` + +Temporarily replaces the mock implementation while the callback is executed, then restores the original implementation. + +```ts +const fn = rstest.fn(() => 1); +fn.withImplementation( + () => 2, + () => { + expect(fn()).toBe(2); + }, +); +expect(fn()).toBe(1); +``` + +## mockReturnThis + +- **Type:** `() => this` + +Makes the mock return `this` when called. + +```ts +const fn = rstest.fn(); +fn.mockReturnThis(); +const obj = { fn }; +expect(obj.fn()).toBe(obj); +``` + +## mockReturnValue + +- **Type:** `(value: any) => MockInstance` + +Makes the mock always return the specified value. + +```ts +const fn = rstest.fn(); +fn.mockReturnValue(42); +expect(fn()).toBe(42); +``` + +## mockReturnValueOnce + +- **Type:** `(value: any) => MockInstance` + +Makes the mock return the specified value for the next call only. + +```ts +const fn = rstest.fn(); +fn.mockReturnValueOnce(1); +expect(fn()).toBe(1); +expect(fn()).toBe(undefined); +``` + +## mockResolvedValue + +- **Type:** `(value: any) => MockInstance` + +Makes the mock return a Promise that resolves to the specified value. + +```ts +const fn = rstest.fn(); +fn.mockResolvedValue(123); +await expect(fn()).resolves.toBe(123); +``` + +## mockResolvedValueOnce + +- **Type:** `(value: any) => MockInstance` + +Makes the mock return a Promise that resolves to the specified value for the next call only. + +```ts +const fn = rstest.fn(); +fn.mockResolvedValueOnce(1); +await expect(fn()).resolves.toBe(1); +await expect(fn()).resolves.toBe(undefined); +``` + +## mockRejectedValue + +- **Type:** `(error: any) => MockInstance` + +Makes the mock return a Promise that rejects with the specified error. + +```ts +const fn = rstest.fn(); +fn.mockRejectedValue(new Error('fail')); +await expect(fn()).rejects.toThrow('fail'); +``` + +## mockRejectedValueOnce + +- **Type:** `(error: any) => MockInstance` + +Makes the mock return a Promise that rejects with the specified error for the next call only. + +```ts +const fn = rstest.fn(); +fn.mockRejectedValueOnce(new Error('fail')); +await expect(fn()).rejects.toThrow('fail'); +await expect(fn()).resolves.toBe(undefined); +``` + +## mock + +The context of the mock, including call arguments, return values, instances, contexts, etc. + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +expect(fn.mock.calls[0]).toEqual([1, 2]); +``` + +### mock.calls + +- **Type:** `Array>` + +An array containing the arguments for each call to the mock function. + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +fn(3, 4); +console.log(fn.mock.calls); // [[1, 2], [3, 4]] +``` + +### mock.instances + +- **Type:** `Array>` + +An array containing the instances that have been instantiated from the mock (when used as a constructor). + +```ts +const Fn = rstest.fn(function () { + this.x = 1; +}); +const a = new Fn(); +const b = new Fn(); +console.log(Fn.mock.instances); // [a, b] +``` + +### mock.contexts + +- **Type:** `Array>` + +An array containing the `this` context for each call to the mock function. + +```ts +const fn = vi.fn(); +const context = {}; + +fn.apply(context); +fn.call(context); + +fn.mock.contexts[0] === context; +fn.mock.contexts[1] === context; +``` + +### mock.invocationCallOrder + +- **Type:** `Array` + +An array of numbers representing the order in which the mock was called, shared across all mocks. The index starts from `1`. + +```ts +const fn1 = rstest.fn(); +const fn2 = rstest.fn(); +fn1(); +fn2(); +fn1(); +console.log(fn1.mock.invocationCallOrder); // [1, 3] +console.log(fn2.mock.invocationCallOrder); // [2] +``` + +### mock.lastCall + +- **Type:** `Parameters | undefined` + +The arguments of the last call to the mock function, or `undefined` if it has not been called. + +```ts +const fn = rstest.fn(); +fn(1, 2); +fn(3, 4); +console.log(fn.mock.lastCall); // [3, 4] +``` + +### mock.results + +- **Type:** `Array>>` + +An array containing the result of each call to the mock function, including returned values, thrown errors, or incomplete calls. + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +try { + fn(); + throw new Error('fail'); +} catch {} +console.log(fn.mock.results); +// [{ type: 'return', value: 3 }, { type: 'throw', value: Error }] +``` + +### mock.settledResults + +- **Type:** `Array>>>` + +An array containing the settled results (fulfilled or rejected) of all async calls to the mock function. + +```ts +const fn = rstest.fn(async (x) => { + if (x > 0) return x; + throw new Error('fail'); +}); +await fn(1); +try { + await fn(0); +} catch {} +console.log(fn.mock.settledResults); +// [{ type: 'fulfilled', value: 1 }, { type: 'rejected', value: Error }] +``` diff --git a/website/docs/en/config/test/_meta.json b/website/docs/en/config/test/_meta.json index 8a5c7c33..bdc17b68 100644 --- a/website/docs/en/config/test/_meta.json +++ b/website/docs/en/config/test/_meta.json @@ -14,5 +14,8 @@ "printConsoleTrace", "onConsoleLog", "disableConsoleIntercept", - "update" + "update", + "clearMocks", + "resetMocks", + "restoreMocks" ] diff --git a/website/docs/en/config/test/clearMocks.mdx b/website/docs/en/config/test/clearMocks.mdx new file mode 100644 index 00000000..1cf807d6 --- /dev/null +++ b/website/docs/en/config/test/clearMocks.mdx @@ -0,0 +1,16 @@ +# clearMocks + +- **Type:** `boolean` +- **Default:** `false` + +Automatically clear mock calls, instances, contexts and results before every test. + +When `clearMocks` enabled, rstest will call [.clearAllMocks()](/api/rstest/mockFunctions#rstestclearallmocks) before each test. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + clearMocks: true, +}); +``` diff --git a/website/docs/en/config/test/resetMocks.mdx b/website/docs/en/config/test/resetMocks.mdx new file mode 100644 index 00000000..eaf08d4a --- /dev/null +++ b/website/docs/en/config/test/resetMocks.mdx @@ -0,0 +1,16 @@ +# resetMocks + +- **Type:** `boolean` +- **Default:** `false` + +Automatically reset mock state before every test. + +When `resetMocks` enabled, rstest will call [.resetAllMocks()](/api/rstest/mockFunctions#rstestresetallmocks) before each test. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + resetMocks: true, +}); +``` diff --git a/website/docs/en/config/test/restoreMocks.mdx b/website/docs/en/config/test/restoreMocks.mdx new file mode 100644 index 00000000..dffd7a0f --- /dev/null +++ b/website/docs/en/config/test/restoreMocks.mdx @@ -0,0 +1,16 @@ +# restoreMocks + +- **Type:** `boolean` +- **Default:** `false` + +Automatically reset mock state before every test. + +When `restoreMocks` enabled, rstest will call [.restoreAllMocks()](/api/rstest/mockFunctions#rstestrestoreallmocks) before each test. + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + restoreMocks: true, +}); +``` diff --git a/website/docs/zh/api/rstest/_meta.json b/website/docs/zh/api/rstest/_meta.json index f6eb67ef..f300f81e 100644 --- a/website/docs/zh/api/rstest/_meta.json +++ b/website/docs/zh/api/rstest/_meta.json @@ -1 +1 @@ -["mockFunctions", "fakeTimers"] +["mockFunctions", "mockInstance", "fakeTimers"] diff --git a/website/docs/zh/api/rstest/mockFunctions.mdx b/website/docs/zh/api/rstest/mockFunctions.mdx index db1c47d8..1119d220 100644 --- a/website/docs/zh/api/rstest/mockFunctions.mdx +++ b/website/docs/zh/api/rstest/mockFunctions.mdx @@ -8,6 +8,16 @@ Rstest 提供了一些工具方法帮助你进行函数的模拟(mock)。 ## rstest.fn +- **类型:** + +```ts +export interface Mock extends MockInstance { + (...args: Parameters): ReturnType; +} + +export type MockFn = (fn?: T) => Mock; +``` + 创建一个 mock 函数。 ```ts @@ -16,10 +26,22 @@ const sayHi = rstest.fn((name: string) => `hi ${name}`); const res = sayHi('bob'); expect(res).toBe('hi bob'); + +expect(sayHi).toHaveBeenCalledTimes(1); ``` ## rstest.spyOn +- **类型:** + +```ts +export type SpyFn = ( + obj: Record, + methodName: string, + accessType?: 'get' | 'set', +) => MockInstance; +``` + 对一个对象的方法进行 mock。 ```ts @@ -37,16 +59,24 @@ expect(spy).toHaveBeenCalled(); ## rstest.isMockFunction +- **类型:** `(fn: any) => fn is MockInstance` + 判断给定的函数是否为 mock 函数。 ## rstest.clearAllMocks +- **类型:** `() => Rstest` + 清除所有 mock 的 `mock.calls`、`mock.instances`、`mock.contexts` 和 `mock.results` 属性。 ## rstest.resetAllMocks +- **类型:** `() => Rstest` + 清除所有 mock 属性,并将每个 mock 的实现重置为其原始实现。 ## rstest.restoreAllMocks +- **类型:** `() => Rstest` + 重置所有 mock,并恢复被 mock 的对象的原始描述符。 diff --git a/website/docs/zh/api/rstest/mockInstance.mdx b/website/docs/zh/api/rstest/mockInstance.mdx new file mode 100644 index 00000000..768e5cd1 --- /dev/null +++ b/website/docs/zh/api/rstest/mockInstance.mdx @@ -0,0 +1,328 @@ +--- +title: MockInstance +overviewHeaders: [2, 3] +--- + +# MockInstance + +`MockInstance` 是所有 mock 和 spy 实例的类型。 + +## getMockName + +- **类型:** `() => string` + +返回通过 `.mockName()` 设置的 mock 名称字符串。 + +```ts +const fn = rstest.fn(); +fn.mockName('myMock'); +expect(fn.getMockName()).toBe('myMock'); +``` + +## mockName + +- **类型:** `(name: string) => MockInstance` + +为此 mock 实例设置名称,便于调试和输出。 + +```ts +const fn = rstest.fn(); +fn.mockName('logger'); +``` + +## mockClear + +- **类型:** `() => MockInstance` + +清除所有关于每次调用的信息(调用、实例、上下文、结果等)。 + +```ts +const fn = rstest.fn(); +fn(1); +fn.mockClear(); +expect(fn.mock.calls.length).toBe(0); +``` + +## mockReset + +- **类型:** `() => MockInstance` + +清除所有调用信息,并将实现重置为初始状态。 + +```ts +const fn = rstest.fn().mockImplementation(() => 1); +fn.mockReset(); +// 实现已被重置 +``` + +## mockRestore + +- **类型:** `() => MockInstance` + +恢复被 spy 的对象的原始方法(仅对 spy 有效)。 + +```ts +const obj = { foo: () => 1 }; +const spy = rstest.spyOn(obj, 'foo'); +spy.mockRestore(); +``` + +## getMockImplementation + +- **类型:** `() => Function | undefined` + +返回当前的 mock 实现函数(如有)。 + +```ts +const fn = rstest.fn(() => 123); +const impl = fn.getMockImplementation(); +``` + +## mockImplementation + +- **类型:** `(fn: Function) => MockInstance` + +为 mock 设置实现函数。 + +```ts +const fn = rstest.fn(); +fn.mockImplementation((a, b) => a + b); +``` + +## mockImplementationOnce + +- **类型:** `(fn: Function) => MockInstance` + +仅为下一次调用设置实现函数。 + +```ts +const fn = rstest.fn(); +fn.mockImplementationOnce(() => 1); +fn(); // 返回 1 +fn(); // 返回 undefined +``` + +## withImplementation + +- **类型:** `(fn: Function, callback: () => any) => void | Promise` + +临时替换 mock 的实现函数,在 callback 执行期间生效,执行完毕后恢复原实现。 + +```ts +const fn = rstest.fn(() => 1); +fn.withImplementation( + () => 2, + () => { + expect(fn()).toBe(2); + }, +); +expect(fn()).toBe(1); +``` + +## mockReturnThis + +- **类型:** `() => this` + +使 mock 在调用时返回 `this`。 + +```ts +const fn = rstest.fn(); +fn.mockReturnThis(); +const obj = { fn }; +expect(obj.fn()).toBe(obj); +``` + +## mockReturnValue + +- **类型:** `(value: any) => MockInstance` + +使 mock 总是返回指定的值。 + +```ts +const fn = rstest.fn(); +fn.mockReturnValue(42); +expect(fn()).toBe(42); +``` + +## mockReturnValueOnce + +- **类型:** `(value: any) => MockInstance` + +使 mock 仅在下一次调用时返回指定的值。 + +```ts +const fn = rstest.fn(); +fn.mockReturnValueOnce(1); +expect(fn()).toBe(1); +expect(fn()).toBe(undefined); +``` + +## mockResolvedValue + +- **类型:** `(value: any) => MockInstance` + +使 mock 返回一个 Promise,resolve 为指定的值。 + +```ts +const fn = rstest.fn(); +fn.mockResolvedValue(123); +await expect(fn()).resolves.toBe(123); +``` + +## mockResolvedValueOnce + +- **类型:** `(value: any) => MockInstance` + +使 mock 仅在下一次调用时返回一个 Promise,resolve 为指定的值。 + +```ts +const fn = rstest.fn(); +fn.mockResolvedValueOnce(1); +await expect(fn()).resolves.toBe(1); +await expect(fn()).resolves.toBe(undefined); +``` + +## mockRejectedValue + +- **类型:** `(error: any) => MockInstance` + +使 mock 返回一个 Promise,reject 为指定的错误。 + +```ts +const fn = rstest.fn(); +fn.mockRejectedValue(new Error('fail')); +await expect(fn()).rejects.toThrow('fail'); +``` + +## mockRejectedValueOnce + +- **类型:** `(error: any) => MockInstance` + +使 mock 仅在下一次调用时返回一个 Promise,reject 为指定的错误。 + +```ts +const fn = rstest.fn(); +fn.mockRejectedValueOnce(new Error('fail')); +await expect(fn()).rejects.toThrow('fail'); +await expect(fn()).resolves.toBe(undefined); +``` + +## mock + +mock 的上下文,包括调用参数、返回值、实例、上下文等。 + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +expect(fn.mock.calls[0]).toEqual([1, 2]); +``` + +### mock.calls + +- **类型:** `Array>` + +包含每次调用 mock 函数的参数的数组。 + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +fn(3, 4); +console.log(fn.mock.calls); // [[1, 2], [3, 4]] +``` + +### mock.instances + +- **类型:** `Array>` + +包含通过 mock 作为构造函数实例化的所有实例的数组。 + +```ts +const Fn = rstest.fn(function () { + this.x = 1; +}); +const a = new Fn(); +const b = new Fn(); +console.log(Fn.mock.instances); // [a, b] +``` + +### mock.contexts + +- **类型:** `Array>` + +包含每次调用 mock 函数时的 `this` 上下文的数组。 + +```ts +const fn = vi.fn(); +const context = {}; + +fn.apply(context); +fn.call(context); + +fn.mock.contexts[0] === context; +fn.mock.contexts[1] === context; +``` + +### mock.invocationCallOrder + +- **类型:** `Array` + +表示 mock 被调用顺序的数字数组,所有 mock 共享。索引从 `1` 开始。 + +```ts +const fn1 = rstest.fn(); +const fn2 = rstest.fn(); +fn1(); +fn2(); +fn1(); +console.log(fn1.mock.invocationCallOrder); // [1, 3] +console.log(fn2.mock.invocationCallOrder); // [2] +``` + +### mock.lastCall + +- **类型:** `Parameters | undefined` + +mock 函数最后一次调用的参数,若未调用则为 `undefined`。 + +```ts +const fn = rstest.fn(); +fn(1, 2); +fn(3, 4); +console.log(fn.mock.lastCall); // [3, 4] +``` + +### mock.results + +- **类型:** `Array>>` + +包含每次调用 mock 函数的结果(返回值、抛出的错误或未完成调用)的数组。 + +```ts +const fn = rstest.fn((a, b) => a + b); +fn(1, 2); +try { + fn(); + throw new Error('fail'); +} catch {} +console.log(fn.mock.results); +// [{ type: 'return', value: 3 }, { type: 'throw', value: Error }] +``` + +### mock.settledResults + +- **类型:** `Array>>>` + +包含所有异步调用的最终结果(fulfilled 或 rejected)的数组。 + +```ts +const fn = rstest.fn(async (x) => { + if (x > 0) return x; + throw new Error('fail'); +}); +await fn(1); +try { + await fn(0); +} catch {} +console.log(fn.mock.settledResults); +// [{ type: 'fulfilled', value: 1 }, { type: 'rejected', value: Error }] +``` diff --git a/website/docs/zh/config/test/_meta.json b/website/docs/zh/config/test/_meta.json index 8a5c7c33..bdc17b68 100644 --- a/website/docs/zh/config/test/_meta.json +++ b/website/docs/zh/config/test/_meta.json @@ -14,5 +14,8 @@ "printConsoleTrace", "onConsoleLog", "disableConsoleIntercept", - "update" + "update", + "clearMocks", + "resetMocks", + "restoreMocks" ] diff --git a/website/docs/zh/config/test/clearMocks.mdx b/website/docs/zh/config/test/clearMocks.mdx new file mode 100644 index 00000000..ae866952 --- /dev/null +++ b/website/docs/zh/config/test/clearMocks.mdx @@ -0,0 +1,16 @@ +# clearMocks + +- **类型:** `boolean` +- **默认值:** `false` + +清除所有 mock 的 `mock.calls`、`mock.instances`、`mock.contexts` 和 `mock.results` 属性。 + +当启用 `clearMocks` 时,rstest 将在每个测试用例执行之前调用 [.clearAllMocks()](/api/rstest/mockFunctions#rstestclearallmocks)。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + clearMocks: true, +}); +``` diff --git a/website/docs/zh/config/test/resetMocks.mdx b/website/docs/zh/config/test/resetMocks.mdx new file mode 100644 index 00000000..ee4f2551 --- /dev/null +++ b/website/docs/zh/config/test/resetMocks.mdx @@ -0,0 +1,16 @@ +# resetMocks + +- **类型:** `boolean` +- **默认值:** `false` + +清除所有 mock 属性,并将每个 mock 的实现重置为其原始实现。 + +当启用 `resetMocks` 时,rstest 将在每个测试用例执行之前调用 [.resetAllMocks()](/api/rstest/mockFunctions#rstestresetallmocks)。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + resetMocks: true, +}); +``` diff --git a/website/docs/zh/config/test/restoreMocks.mdx b/website/docs/zh/config/test/restoreMocks.mdx new file mode 100644 index 00000000..b384d6ed --- /dev/null +++ b/website/docs/zh/config/test/restoreMocks.mdx @@ -0,0 +1,16 @@ +# restoreMocks + +- **类型:** `boolean` +- **默认值:** `false` + +重置所有 mock,并恢复被 mock 的对象的原始描述符。 + +当启用 `restoreMocks` 时,rstest 将在每个测试用例执行之前调用 [.restoreAllMocks()](/api/rstest/mockFunctions#rstestrestoreallmocks)。 + +```ts title="rstest.config.ts" +import { defineConfig } from '@rstest/core'; + +export default defineConfig({ + restoreMocks: true, +}); +``` From 36399d36cfc3b0d8223cdcd2b294bc983cb849a8 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 2 Jul 2025 11:46:14 +0800 Subject: [PATCH 05/10] chore(deps): update all patch dependencies (#347) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- examples/react/package.json | 2 +- packages/core/package.json | 6 +- pnpm-lock.yaml | 242 +++++++++++++++--------------- tests/jsdom/fixtures/package.json | 2 +- tests/package.json | 4 +- 5 files changed, 128 insertions(+), 128 deletions(-) diff --git a/examples/react/package.json b/examples/react/package.json index 0e1e44f4..c8d7240c 100644 --- a/examples/react/package.json +++ b/examples/react/package.json @@ -13,7 +13,7 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@rsbuild/core": "1.4.2", + "@rsbuild/core": "1.4.3", "@rsbuild/plugin-react": "^1.3.2", "@testing-library/dom": "^10.4.0", "@testing-library/react": "^16.3.0", diff --git a/packages/core/package.json b/packages/core/package.json index 436aec19..64a9801c 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -54,7 +54,7 @@ "test": "npx rstest run --globals" }, "dependencies": { - "@rsbuild/core": "1.4.2", + "@rsbuild/core": "1.4.3", "@types/chai": "^5.2.2", "@vitest/expect": "^3.2.4", "@vitest/snapshot": "^3.2.4", @@ -67,7 +67,7 @@ "devDependencies": { "@sinonjs/fake-timers": "^14.0.0", "@babel/code-frame": "^7.27.1", - "@jridgewell/trace-mapping": "0.3.27", + "@jridgewell/trace-mapping": "0.3.29", "@microsoft/api-extractor": "^7.52.8", "@rslib/core": "0.10.4", "@rstest/tsconfig": "workspace:*", @@ -80,7 +80,7 @@ "jest-diff": "^30.0.3", "license-webpack-plugin": "^4.0.2", "picocolors": "^1.1.1", - "rslog": "^1.2.8", + "rslog": "^1.2.9", "source-map-support": "^0.5.21", "stacktrace-parser": "0.1.11", "tinyglobby": "^0.2.14", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ee3cdd0a..90265240 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,7 +16,7 @@ importers: version: 2.29.5 '@rsdoctor/rspack-plugin': specifier: ^1.1.5 - version: 1.1.5(@rsbuild/core@1.4.2)(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + version: 1.1.5(@rsbuild/core@1.4.3)(@rspack/core@1.4.2(@swc/helpers@0.5.17)) '@rstest/core': specifier: workspace:* version: link:packages/core @@ -85,11 +85,11 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@rsbuild/core': - specifier: 1.4.2 - version: 1.4.2 + specifier: 1.4.3 + version: 1.4.3 '@rsbuild/plugin-react': specifier: ^1.3.2 - version: 1.3.2(@rsbuild/core@1.4.2) + version: 1.3.2(@rsbuild/core@1.4.3) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -112,8 +112,8 @@ importers: packages/core: dependencies: '@rsbuild/core': - specifier: 1.4.2 - version: 1.4.2 + specifier: 1.4.3 + version: 1.4.3 '@types/chai': specifier: ^5.2.2 version: 5.2.2 @@ -143,8 +143,8 @@ importers: specifier: ^7.27.1 version: 7.27.1 '@jridgewell/trace-mapping': - specifier: 0.3.27 - version: 0.3.27 + specifier: 0.3.29 + version: 0.3.29 '@microsoft/api-extractor': specifier: ^7.52.8 version: 7.52.8(@types/node@22.13.8) @@ -185,8 +185,8 @@ importers: specifier: ^1.1.1 version: 1.1.1 rslog: - specifier: ^1.2.8 - version: 1.2.8 + specifier: ^1.2.9 + version: 1.2.9 source-map-support: specifier: ^0.5.21 version: 0.5.21 @@ -205,8 +205,8 @@ importers: tests: devDependencies: '@rsbuild/core': - specifier: 1.4.2 - version: 1.4.2 + specifier: 1.4.3 + version: 1.4.3 '@rslib/core': specifier: 0.10.4 version: 0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(typescript@5.8.3) @@ -220,7 +220,7 @@ importers: specifier: ^6.4.0 version: 6.4.0 axios: - specifier: ^1.9.0 + specifier: ^1.10.0 version: 1.10.0 jest-image-snapshot: specifier: ^6.5.1 @@ -297,11 +297,11 @@ importers: version: 19.1.0(react@19.1.0) devDependencies: '@rsbuild/core': - specifier: 1.4.2 - version: 1.4.2 + specifier: 1.4.3 + version: 1.4.3 '@rsbuild/plugin-react': specifier: ^1.3.2 - version: 1.3.2(@rsbuild/core@1.4.2) + version: 1.3.2(@rsbuild/core@1.4.3) '@testing-library/dom': specifier: ^10.4.0 version: 10.4.0 @@ -359,7 +359,7 @@ importers: devDependencies: '@rsbuild/plugin-react': specifier: ^1.3.2 - version: 1.3.2(@rsbuild/core@1.4.2) + version: 1.3.2(@rsbuild/core@1.4.3) '@types/react': specifier: ^19.1.8 version: 19.1.8 @@ -380,7 +380,7 @@ importers: devDependencies: '@rsbuild/plugin-sass': specifier: ^1.3.2 - version: 1.3.2(@rsbuild/core@1.4.2) + version: 1.3.2(@rsbuild/core@1.4.3) '@rspress/plugin-llms': specifier: 2.0.0-beta.18 version: 2.0.0-beta.18(@rspress/core@2.0.0-beta.18(@types/react@19.1.8)(acorn@8.14.1)) @@ -407,10 +407,10 @@ importers: version: 19.1.0(react@19.1.0) rsbuild-plugin-google-analytics: specifier: ^1.0.3 - version: 1.0.3(@rsbuild/core@1.4.2) + version: 1.0.3(@rsbuild/core@1.4.3) rsbuild-plugin-open-graph: specifier: ^1.0.2 - version: 1.0.2(@rsbuild/core@1.4.2) + version: 1.0.2(@rsbuild/core@1.4.3) rspress: specifier: ^2.0.0-beta.18 version: 2.0.0-beta.18(@types/react@19.1.8)(acorn@8.14.1) @@ -693,8 +693,8 @@ packages: '@jridgewell/sourcemap-codec@1.5.0': resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==} - '@jridgewell/trace-mapping@0.3.27': - resolution: {integrity: sha512-VO95AxtSFMelbg3ouljAYnfvTEwSWVt/2YLf+U5Ejd8iT5mXE2Sa/1LGyvySMne2CGsepGLI7KpF3EzE3Aq9Mg==} + '@jridgewell/trace-mapping@0.3.29': + resolution: {integrity: sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==} '@jsonjoy.com/base64@1.1.2': resolution: {integrity: sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==} @@ -852,8 +852,8 @@ packages: resolution: {integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==} engines: {node: '>=14.0.0'} - '@rsbuild/core@1.4.2': - resolution: {integrity: sha512-46rcBPYz2kIdDQ1en40yDRT7ZGOKUB0+NqeOAvMAg4DU7TCfgK1qJdmznVasagTWKN9RjjzNpy5encS6W7gUOQ==} + '@rsbuild/core@1.4.3': + resolution: {integrity: sha512-97vmVaOXUxID85cVSDFHLFmDfeJTR4SoOHbn7kknkEeZFg3wHlDYhx+lbQPOZf+toHOm8d1w1LlunxVkCAdHLg==} engines: {node: '>=16.10.0'} hasBin: true @@ -922,60 +922,60 @@ packages: typescript: optional: true - '@rspack/binding-darwin-arm64@1.4.1': - resolution: {integrity: sha512-enh5DYbpaexdEmjbcxj3BJDauP3w+20jFKWvKROtAQV350PUw0bf2b4WOgngIH9hBzlfjpXNYAk6T5AhVAlY3Q==} + '@rspack/binding-darwin-arm64@1.4.2': + resolution: {integrity: sha512-0fPOew7D0l/x6qFZYdyUqutbw15K98VLvES2/7x2LPssTgypE4rVmnQSmVBnge3Nr8Qs/9qASPRpMWXBaqMfOA==} cpu: [arm64] os: [darwin] - '@rspack/binding-darwin-x64@1.4.1': - resolution: {integrity: sha512-KoehyhBji4TLXhn4mqOUw6xsQNRzNVA9XcCm1Jx+M1Qb0dhMTNfduvBSyXuRV5+/QaRbk7+4UJbyRNFUtt96kA==} + '@rspack/binding-darwin-x64@1.4.2': + resolution: {integrity: sha512-0Dh6ssGgwnd9G+IO8SwQaJ0RJ8NkQbk4hwoJH/u52Mnfl0EvhmNvuhkbSEoKn1U3kElOA2cxH/3gbYzuYExn3g==} cpu: [x64] os: [darwin] - '@rspack/binding-linux-arm64-gnu@1.4.1': - resolution: {integrity: sha512-PJ5cHqvrj1bK7jH5DVrdKoR8Fy+p6l9baxXajq/6xWTxP+4YTdEtLsRZnpLMS1Ho2RRpkxDWJn+gdlKuleNioQ==} + '@rspack/binding-linux-arm64-gnu@1.4.2': + resolution: {integrity: sha512-UHAzggS8Mc7b3Xguhj82HwujLqBZquCeo8qJj5XreNaMKGb6YRw/91dJOVmkNiLCB0bj71CRE1Cocd+Peq3N9A==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-arm64-musl@1.4.1': - resolution: {integrity: sha512-cpDz+z3FwVQfK6VYfXQEb0ym6fFIVmvK4y3R/2VAbVGWYVxZB5I6AcSdOWdDnpppHmcHpf+qQFlwhHvbpMMJNQ==} + '@rspack/binding-linux-arm64-musl@1.4.2': + resolution: {integrity: sha512-QybZ0VxlFih+upLoE7Le5cN3LpxJwk6EnEQTigmzpfc4c4SOC889ftBoIAO3IeBk+mF3H2C9xD+/NolTdwoeiw==} cpu: [arm64] os: [linux] - '@rspack/binding-linux-x64-gnu@1.4.1': - resolution: {integrity: sha512-jjTx53CpiYWK7fAv5qS8xHEytFK6gLfZRk+0kt2YII6uqez/xQ3SRcboreH8XbJcBoxINBzMNMf5/SeMBZ939A==} + '@rspack/binding-linux-x64-gnu@1.4.2': + resolution: {integrity: sha512-ucCCWdtH1tekZadrsYj6GNJ8EP21BM2uSE7MootbwLw8aBtgVTKUuRDQEps1h/rtrdthzd9XBX6Lc2N926gM+g==} cpu: [x64] os: [linux] - '@rspack/binding-linux-x64-musl@1.4.1': - resolution: {integrity: sha512-FAyR3Og81Smtr/CnsuTiW4ZCYAPCqeV73lzMKZ9xdVUgM9324ryEgqgX38GZLB5Mo7cvQhv7/fpMeHQo16XQCw==} + '@rspack/binding-linux-x64-musl@1.4.2': + resolution: {integrity: sha512-+Y2LS6Qyk2AZor8DqlA8yKCqElYr0Urjc3M66O4ZzlxDT5xXX0J2vp04AtFp0g81q/+UgV3cbC//dqDvO0SiBA==} cpu: [x64] os: [linux] - '@rspack/binding-wasm32-wasi@1.4.1': - resolution: {integrity: sha512-3Q1VICIQP4GsaTJEmmwfowQ48NvhlL0CKH88l5+mbji2rBkGx7yR67pPdfCVNjXcCtFoemTYw98eaumJTjC++g==} + '@rspack/binding-wasm32-wasi@1.4.2': + resolution: {integrity: sha512-3WvfHY7NvzORek3FcQWLI/B8wQ7NZe0e0Bub9GyLNVxe5Bi+dxnSzEg6E7VsjbUzKnYufJA0hDKbEJ2qCMvpdw==} cpu: [wasm32] - '@rspack/binding-win32-arm64-msvc@1.4.1': - resolution: {integrity: sha512-DdLPOy1J98kn45uEhiEqlBKgMvet+AxOzX2OcrnU0wQXthGM9gty1YXYNryOhlK+X+eOcwcP3GbnDOAKi8nKqw==} + '@rspack/binding-win32-arm64-msvc@1.4.2': + resolution: {integrity: sha512-Y6L9DrLFRW6qBBCY3xBt7townStN5mlcbBTuG1zeXl0KcORPv1G1Cq6HXP6f1em+YsHE1iwnNqLvv4svg5KsnQ==} cpu: [arm64] os: [win32] - '@rspack/binding-win32-ia32-msvc@1.4.1': - resolution: {integrity: sha512-13s8fYtyC9DyvKosD2Kvzd6fVZDZZyPp91L4TEXWaO0CFhaCbtLTYIntExq9MwtKHYKKx7bchIFw93o0xjKjUg==} + '@rspack/binding-win32-ia32-msvc@1.4.2': + resolution: {integrity: sha512-FyTJrL7GcYXPWKUB9Oj2X29kfma6MUgM9PyXGy8gDMti21kMMhpHp/bGVqfurRbazDyklDuLLtbHuawpa6toeA==} cpu: [ia32] os: [win32] - '@rspack/binding-win32-x64-msvc@1.4.1': - resolution: {integrity: sha512-ubQW8FcLnwljDanwTzkC9Abyo59gmX8m9uVr1GHOEuEU9Cua0KMijX2j/MYfiziz4nuQgv1saobY7K1I5nE3YA==} + '@rspack/binding-win32-x64-msvc@1.4.2': + resolution: {integrity: sha512-ODSU26tmG8MfMFDHCaMLCORB64EVdEtDvPP5zJs0Mgh7vQaqweJtqgG0ukZCQy4ApUatOrMaZrLk557jp9Biyw==} cpu: [x64] os: [win32] - '@rspack/binding@1.4.1': - resolution: {integrity: sha512-zYgOmI+LC2zxB/LIcnaeK66ElFHaPChdWzRruTT1LAFFwpgGkBGAwFoP27PDnxQW0Aejci21Ld8X9tyxm08QFw==} + '@rspack/binding@1.4.2': + resolution: {integrity: sha512-NdTLlA20ufD0thFvDIwwPk+bX9yo3TDE4XjfvZYbwFyYvBgqJOWQflnbwLgvSTck0MSTiOqWIqpR88ymAvWTqg==} - '@rspack/core@1.4.1': - resolution: {integrity: sha512-UTRCTQk2G8YiPBiMvfn8FcysxeO4Muek6a/Z39Cw2r4ZI8k5iPnKiyZboTJLS7120PwWBw2SO+QQje35Z44x0g==} + '@rspack/core@1.4.2': + resolution: {integrity: sha512-Mmk3X3fbOLtRq4jX8Ebp3rfjr75YgupvNksQb0WbaGEVr5l1b6woPH/LaXF2v9U9DP83wmpZJXJ8vclB5JfL/w==} engines: {node: '>=16.0.0'} peerDependencies: '@swc/helpers': '>=0.5.1' @@ -3182,8 +3182,8 @@ packages: '@rsbuild/core': optional: true - rslog@1.2.8: - resolution: {integrity: sha512-BXUB5LnElxG0n9dSS+1Num4q+U+GGuCasi2/8I6hYMyZm2+L5kUGvv7pAc6z7+ODxFXVV6AHy9mSa2VSoauk+g==} + rslog@1.2.9: + resolution: {integrity: sha512-KSjM8jJKYYaKgI4jUGZZ4kdTBTM/EIGH1JnoB0ptMkzcyWaHeXW9w6JVLCYs37gh8sFZkLLqAyBb2sT02bqpcQ==} rspack-plugin-virtual-module@1.0.1: resolution: {integrity: sha512-NQJ3fXa1v0WayvfHMWbyqLUA3JIqgCkhIcIOnZscuisinxorQyIAo+bqcU5pCusMKSyPqVIWO3caQyl0s9VDAg==} @@ -4210,7 +4210,7 @@ snapshots: '@jridgewell/sourcemap-codec@1.5.0': {} - '@jridgewell/trace-mapping@0.3.27': + '@jridgewell/trace-mapping@0.3.29': dependencies: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.0 @@ -4418,15 +4418,15 @@ snapshots: '@remix-run/router@1.23.0': {} - '@rsbuild/core@1.4.2': + '@rsbuild/core@1.4.3': dependencies: - '@rspack/core': 1.4.1(@swc/helpers@0.5.17) + '@rspack/core': 1.4.2(@swc/helpers@0.5.17) '@rspack/lite-tapable': 1.0.1 '@swc/helpers': 0.5.17 core-js: 3.43.0 jiti: 2.4.2 - '@rsbuild/plugin-check-syntax@1.3.0(@rsbuild/core@1.4.2)': + '@rsbuild/plugin-check-syntax@1.3.0(@rsbuild/core@1.4.3)': dependencies: acorn: 8.14.1 browserslist-to-es-version: 1.0.0 @@ -4434,19 +4434,19 @@ snapshots: picocolors: 1.1.1 source-map: 0.7.4 optionalDependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 - '@rsbuild/plugin-react@1.3.2(@rsbuild/core@1.4.2)': + '@rsbuild/plugin-react@1.3.2(@rsbuild/core@1.4.3)': dependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 '@rspack/plugin-react-refresh': 1.4.3(react-refresh@0.17.0) react-refresh: 0.17.0 transitivePeerDependencies: - webpack-hot-middleware - '@rsbuild/plugin-sass@1.3.2(@rsbuild/core@1.4.2)': + '@rsbuild/plugin-sass@1.3.2(@rsbuild/core@1.4.3)': dependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 deepmerge: 4.3.1 loader-utils: 2.0.4 postcss: 8.5.4 @@ -4455,13 +4455,13 @@ snapshots: '@rsdoctor/client@1.1.5': {} - '@rsdoctor/core@1.1.5(@rsbuild/core@1.4.2)(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/core@1.1.5(@rsbuild/core@1.4.3)(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: - '@rsbuild/plugin-check-syntax': 1.3.0(@rsbuild/core@1.4.2) - '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/sdk': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/types': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + '@rsbuild/plugin-check-syntax': 1.3.0(@rsbuild/core@1.4.3) + '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/sdk': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/types': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) axios: 1.10.0 browserslist-load-config: 1.0.0 enhanced-resolve: 5.12.0 @@ -4481,10 +4481,10 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/graph@1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/graph@1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: - '@rsdoctor/types': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + '@rsdoctor/types': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) lodash.unionby: 4.8.0 socket.io: 4.8.1 source-map: 0.7.4 @@ -4495,16 +4495,16 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/rspack-plugin@1.1.5(@rsbuild/core@1.4.2)(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/rspack-plugin@1.1.5(@rsbuild/core@1.4.3)(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: - '@rsdoctor/core': 1.1.5(@rsbuild/core@1.4.2)(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/sdk': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/types': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + '@rsdoctor/core': 1.1.5(@rsbuild/core@1.4.3)(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/sdk': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/types': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) lodash: 4.17.21 optionalDependencies: - '@rspack/core': 1.4.1(@swc/helpers@0.5.17) + '@rspack/core': 1.4.2(@swc/helpers@0.5.17) transitivePeerDependencies: - '@rsbuild/core' - bufferutil @@ -4513,12 +4513,12 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/sdk@1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/sdk@1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: '@rsdoctor/client': 1.1.5 - '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/types': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) - '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + '@rsdoctor/graph': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/types': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) + '@rsdoctor/utils': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) '@types/fs-extra': 11.0.4 body-parser: 1.20.3 cors: 2.8.5 @@ -4538,19 +4538,19 @@ snapshots: - utf-8-validate - webpack - '@rsdoctor/types@1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/types@1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: '@types/connect': 3.4.38 '@types/estree': 1.0.5 '@types/tapable': 2.2.7 source-map: 0.7.4 optionalDependencies: - '@rspack/core': 1.4.1(@swc/helpers@0.5.17) + '@rspack/core': 1.4.2(@swc/helpers@0.5.17) - '@rsdoctor/utils@1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17))': + '@rsdoctor/utils@1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17))': dependencies: '@babel/code-frame': 7.26.2 - '@rsdoctor/types': 1.1.5(@rspack/core@1.4.1(@swc/helpers@0.5.17)) + '@rsdoctor/types': 1.1.5(@rspack/core@1.4.2(@swc/helpers@0.5.17)) '@types/estree': 1.0.5 acorn: 8.14.1 acorn-import-attributes: 1.9.5(acorn@8.14.1) @@ -4564,7 +4564,7 @@ snapshots: json-stream-stringify: 3.0.1 lines-and-columns: 2.0.4 picocolors: 1.1.1 - rslog: 1.2.8 + rslog: 1.2.9 strip-ansi: 6.0.1 transitivePeerDependencies: - '@rspack/core' @@ -4573,62 +4573,62 @@ snapshots: '@rslib/core@0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(typescript@5.8.3)': dependencies: - '@rsbuild/core': 1.4.2 - rsbuild-plugin-dts: 0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(@rsbuild/core@1.4.2)(typescript@5.8.3) + '@rsbuild/core': 1.4.3 + rsbuild-plugin-dts: 0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(@rsbuild/core@1.4.3)(typescript@5.8.3) tinyglobby: 0.2.14 optionalDependencies: '@microsoft/api-extractor': 7.52.8(@types/node@22.13.8) typescript: 5.8.3 - '@rspack/binding-darwin-arm64@1.4.1': + '@rspack/binding-darwin-arm64@1.4.2': optional: true - '@rspack/binding-darwin-x64@1.4.1': + '@rspack/binding-darwin-x64@1.4.2': optional: true - '@rspack/binding-linux-arm64-gnu@1.4.1': + '@rspack/binding-linux-arm64-gnu@1.4.2': optional: true - '@rspack/binding-linux-arm64-musl@1.4.1': + '@rspack/binding-linux-arm64-musl@1.4.2': optional: true - '@rspack/binding-linux-x64-gnu@1.4.1': + '@rspack/binding-linux-x64-gnu@1.4.2': optional: true - '@rspack/binding-linux-x64-musl@1.4.1': + '@rspack/binding-linux-x64-musl@1.4.2': optional: true - '@rspack/binding-wasm32-wasi@1.4.1': + '@rspack/binding-wasm32-wasi@1.4.2': dependencies: '@napi-rs/wasm-runtime': 0.2.11 optional: true - '@rspack/binding-win32-arm64-msvc@1.4.1': + '@rspack/binding-win32-arm64-msvc@1.4.2': optional: true - '@rspack/binding-win32-ia32-msvc@1.4.1': + '@rspack/binding-win32-ia32-msvc@1.4.2': optional: true - '@rspack/binding-win32-x64-msvc@1.4.1': + '@rspack/binding-win32-x64-msvc@1.4.2': optional: true - '@rspack/binding@1.4.1': + '@rspack/binding@1.4.2': optionalDependencies: - '@rspack/binding-darwin-arm64': 1.4.1 - '@rspack/binding-darwin-x64': 1.4.1 - '@rspack/binding-linux-arm64-gnu': 1.4.1 - '@rspack/binding-linux-arm64-musl': 1.4.1 - '@rspack/binding-linux-x64-gnu': 1.4.1 - '@rspack/binding-linux-x64-musl': 1.4.1 - '@rspack/binding-wasm32-wasi': 1.4.1 - '@rspack/binding-win32-arm64-msvc': 1.4.1 - '@rspack/binding-win32-ia32-msvc': 1.4.1 - '@rspack/binding-win32-x64-msvc': 1.4.1 - - '@rspack/core@1.4.1(@swc/helpers@0.5.17)': + '@rspack/binding-darwin-arm64': 1.4.2 + '@rspack/binding-darwin-x64': 1.4.2 + '@rspack/binding-linux-arm64-gnu': 1.4.2 + '@rspack/binding-linux-arm64-musl': 1.4.2 + '@rspack/binding-linux-x64-gnu': 1.4.2 + '@rspack/binding-linux-x64-musl': 1.4.2 + '@rspack/binding-wasm32-wasi': 1.4.2 + '@rspack/binding-win32-arm64-msvc': 1.4.2 + '@rspack/binding-win32-ia32-msvc': 1.4.2 + '@rspack/binding-win32-x64-msvc': 1.4.2 + + '@rspack/core@1.4.2(@swc/helpers@0.5.17)': dependencies: '@module-federation/runtime-tools': 0.15.0 - '@rspack/binding': 1.4.1 + '@rspack/binding': 1.4.2 '@rspack/lite-tapable': 1.0.1 optionalDependencies: '@swc/helpers': 0.5.17 @@ -4646,8 +4646,8 @@ snapshots: '@mdx-js/loader': 3.1.0(acorn@8.14.1) '@mdx-js/mdx': 3.1.0(acorn@8.14.1) '@mdx-js/react': 3.1.0(@types/react@19.1.8)(react@19.1.0) - '@rsbuild/core': 1.4.2 - '@rsbuild/plugin-react': 1.3.2(@rsbuild/core@1.4.2) + '@rsbuild/core': 1.4.3 + '@rsbuild/plugin-react': 1.3.2(@rsbuild/core@1.4.3) '@rspress/mdx-rs': 0.6.6 '@rspress/plugin-last-updated': 2.0.0-beta.18 '@rspress/plugin-medium-zoom': 2.0.0-beta.18(@rspress/runtime@2.0.0-beta.18) @@ -4753,7 +4753,7 @@ snapshots: '@rspress/shared@2.0.0-beta.18': dependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 '@shikijs/rehype': 3.4.2 gray-matter: 4.0.3 lodash-es: 4.17.21 @@ -7292,10 +7292,10 @@ snapshots: rrweb-cssom@0.8.0: {} - rsbuild-plugin-dts@0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(@rsbuild/core@1.4.2)(typescript@5.8.3): + rsbuild-plugin-dts@0.10.4(@microsoft/api-extractor@7.52.8(@types/node@22.13.8))(@rsbuild/core@1.4.3)(typescript@5.8.3): dependencies: '@ast-grep/napi': 0.37.0 - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 magic-string: 0.30.17 picocolors: 1.1.1 tinyglobby: 0.2.14 @@ -7304,15 +7304,15 @@ snapshots: '@microsoft/api-extractor': 7.52.8(@types/node@22.13.8) typescript: 5.8.3 - rsbuild-plugin-google-analytics@1.0.3(@rsbuild/core@1.4.2): + rsbuild-plugin-google-analytics@1.0.3(@rsbuild/core@1.4.3): optionalDependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 - rsbuild-plugin-open-graph@1.0.2(@rsbuild/core@1.4.2): + rsbuild-plugin-open-graph@1.0.2(@rsbuild/core@1.4.3): optionalDependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 - rslog@1.2.8: {} + rslog@1.2.9: {} rspack-plugin-virtual-module@1.0.1: dependencies: @@ -7324,7 +7324,7 @@ snapshots: rspress@2.0.0-beta.18(@types/react@19.1.8)(acorn@8.14.1): dependencies: - '@rsbuild/core': 1.4.2 + '@rsbuild/core': 1.4.3 '@rspress/core': 2.0.0-beta.18(@types/react@19.1.8)(acorn@8.14.1) '@rspress/shared': 2.0.0-beta.18 cac: 6.7.14 diff --git a/tests/jsdom/fixtures/package.json b/tests/jsdom/fixtures/package.json index 88cef007..a3740255 100644 --- a/tests/jsdom/fixtures/package.json +++ b/tests/jsdom/fixtures/package.json @@ -11,7 +11,7 @@ "react-dom": "^19.1.0" }, "devDependencies": { - "@rsbuild/core": "1.4.2", + "@rsbuild/core": "1.4.3", "@rsbuild/plugin-react": "^1.3.2", "@testing-library/jest-dom": "^6.6.3", "@testing-library/dom": "^10.4.0", diff --git a/tests/package.json b/tests/package.json index fef0aeca..e66cc93d 100644 --- a/tests/package.json +++ b/tests/package.json @@ -7,12 +7,12 @@ "typecheck": "tsc --noEmit" }, "devDependencies": { - "@rsbuild/core": "1.4.2", + "@rsbuild/core": "1.4.3", "@rslib/core": "0.10.4", "@rstest/core": "workspace:*", "@rstest/tsconfig": "workspace:*", "@types/jest-image-snapshot": "^6.4.0", - "axios": "^1.9.0", + "axios": "^1.10.0", "jest-image-snapshot": "^6.5.1", "pathe": "^2.0.3", "redux": "^5.0.1", From 34239e506e02219048375ad82b4900483ad343d8 Mon Sep 17 00:00:00 2001 From: 9aoy Date: Wed, 2 Jul 2025 11:46:27 +0800 Subject: [PATCH 06/10] docs: add some rstest utilities API (#348) --- scripts/dictionary.txt | 1 + website/docs/en/api/rstest/_meta.json | 2 +- website/docs/en/api/rstest/mockFunctions.mdx | 2 +- website/docs/en/api/rstest/utilities.mdx | 78 +++++++++++++++++++ website/docs/en/api/test-api/_meta.json | 2 +- website/docs/en/config/test/_meta.json | 4 +- website/docs/en/config/test/unstubEnvs.mdx | 10 +++ website/docs/en/config/test/unstubGlobals.mdx | 10 +++ website/docs/zh/api/rstest/_meta.json | 2 +- website/docs/zh/api/rstest/mockFunctions.mdx | 2 +- website/docs/zh/api/rstest/utilities.mdx | 78 +++++++++++++++++++ website/docs/zh/api/test-api/_meta.json | 2 +- website/docs/zh/config/test/_meta.json | 4 +- website/docs/zh/config/test/unstubEnvs.mdx | 10 +++ website/docs/zh/config/test/unstubGlobals.mdx | 10 +++ 15 files changed, 209 insertions(+), 8 deletions(-) create mode 100644 website/docs/en/api/rstest/utilities.mdx create mode 100644 website/docs/en/config/test/unstubEnvs.mdx create mode 100644 website/docs/en/config/test/unstubGlobals.mdx create mode 100644 website/docs/zh/api/rstest/utilities.mdx create mode 100644 website/docs/zh/config/test/unstubEnvs.mdx create mode 100644 website/docs/zh/config/test/unstubGlobals.mdx diff --git a/scripts/dictionary.txt b/scripts/dictionary.txt index 62768905..237abb56 100644 --- a/scripts/dictionary.txt +++ b/scripts/dictionary.txt @@ -136,6 +136,7 @@ templating tinyexec tinyglobby transpiling +tinyspy treeshaking tsbuildinfo tsconfck diff --git a/website/docs/en/api/rstest/_meta.json b/website/docs/en/api/rstest/_meta.json index f300f81e..d3265c1c 100644 --- a/website/docs/en/api/rstest/_meta.json +++ b/website/docs/en/api/rstest/_meta.json @@ -1 +1 @@ -["mockFunctions", "mockInstance", "fakeTimers"] +["mockFunctions", "mockInstance", "fakeTimers", "utilities"] diff --git a/website/docs/en/api/rstest/mockFunctions.mdx b/website/docs/en/api/rstest/mockFunctions.mdx index a1dd9f12..c18f8896 100644 --- a/website/docs/en/api/rstest/mockFunctions.mdx +++ b/website/docs/en/api/rstest/mockFunctions.mdx @@ -4,7 +4,7 @@ title: Mock functions # Mock functions -Rstest provides some utility functions to help you mock functions. +Rstest provides some utility functions to help you mock functions powered by [tinyspy](https://github.com/tinylibs/tinyspy). ## rstest.fn diff --git a/website/docs/en/api/rstest/utilities.mdx b/website/docs/en/api/rstest/utilities.mdx new file mode 100644 index 00000000..0862aabd --- /dev/null +++ b/website/docs/en/api/rstest/utilities.mdx @@ -0,0 +1,78 @@ +# Utilities + +A set of useful utility functions. + +## rstest.stubEnv + +**Type:** `(name: string, value: string | undefined) => Rstest` + +Temporarily sets an environment variable in `process.env` to the specified value. Useful for testing code that depends on environment variables. + +- If `value` is `undefined`, the variable will be removed from `process.env`. +- You can call this multiple times to stub multiple variables. +- Use `rstest.unstubAllEnvs()` to restore all environment variables changed by this method. + +**Example:** + +```ts +rstest.stubEnv('NODE_ENV', 'test'); +expect(process.env.NODE_ENV).toBe('test'); + +rstest.stubEnv('MY_VAR', undefined); +expect(process.env.MY_VAR).toBeUndefined(); +``` + +## rstest.unstubAllEnvs + +**Type:** `() => Rstest` + +Restores all environment variables that were changed using `rstest.stubEnv` to their original values. + +- Call this after your test to clean up any environment changes. +- Automatically called before each test if `unstubEnvs` config is enabled. + +**Example:** + +```ts +rstest.stubEnv('NODE_ENV', 'test'); +// ... run some code +rstest.unstubAllEnvs(); +expect(process.env.NODE_ENV).not.toBe('test'); +``` + +## rstest.stubGlobal + +**Type:** `(name: string | number | symbol, value: unknown) => Rstest` + +Temporarily sets a global variable to the specified value. Useful for mocking global objects or functions. + +- You can call this multiple times to stub multiple globals. +- Use `rstest.unstubAllGlobals()` to restore all globals changed by this method. + +**Example:** + +```ts +rstest.stubGlobal('myGlobal', 123); +expect(globalThis.myGlobal).toBe(123); + +rstest.stubGlobal(Symbol.for('foo'), 'bar'); +expect(globalThis[Symbol.for('foo')]).toBe('bar'); +``` + +## rstest.unstubAllGlobals + +**Type:** `() => Rstest` + +Restores all global variables that were changed using `rstest.stubGlobal` to their original values. + +- Call this after your test to clean up any global changes. +- Automatically called before each test if `unstubGlobals` config is enabled. + +**Example:** + +```ts +rstest.stubGlobal('myGlobal', 123); +// ... run some code +rstest.unstubAllGlobals(); +expect(globalThis.myGlobal).toBeUndefined(); +``` diff --git a/website/docs/en/api/test-api/_meta.json b/website/docs/en/api/test-api/_meta.json index 9442282a..71a6e491 100644 --- a/website/docs/en/api/test-api/_meta.json +++ b/website/docs/en/api/test-api/_meta.json @@ -1 +1 @@ -["describe", "test", "hooks"] +["test", "describe", "hooks"] diff --git a/website/docs/en/config/test/_meta.json b/website/docs/en/config/test/_meta.json index bdc17b68..4a8a504d 100644 --- a/website/docs/en/config/test/_meta.json +++ b/website/docs/en/config/test/_meta.json @@ -17,5 +17,7 @@ "update", "clearMocks", "resetMocks", - "restoreMocks" + "restoreMocks", + "unstubEnvs", + "unstubGlobals" ] diff --git a/website/docs/en/config/test/unstubEnvs.mdx b/website/docs/en/config/test/unstubEnvs.mdx new file mode 100644 index 00000000..17572f7f --- /dev/null +++ b/website/docs/en/config/test/unstubEnvs.mdx @@ -0,0 +1,10 @@ +--- +overviewHeaders: [] +--- + +# unstubEnvs + +- **Type:** `boolean` +- **Default:** `false` + +Restores all `process.env` values that were changed with [rstest.stubEnv](/api/rstest/utilities#rsteststubenv) before every test. diff --git a/website/docs/en/config/test/unstubGlobals.mdx b/website/docs/en/config/test/unstubGlobals.mdx new file mode 100644 index 00000000..e585868a --- /dev/null +++ b/website/docs/en/config/test/unstubGlobals.mdx @@ -0,0 +1,10 @@ +--- +overviewHeaders: [] +--- + +# unstubGlobals + +- **Type:** `boolean` +- **Default:** `false` + +Restores all global variables that were changed with [rstest.stubGlobal](/api/rstest/utilities#rsteststubglobal) before every test. diff --git a/website/docs/zh/api/rstest/_meta.json b/website/docs/zh/api/rstest/_meta.json index f300f81e..d3265c1c 100644 --- a/website/docs/zh/api/rstest/_meta.json +++ b/website/docs/zh/api/rstest/_meta.json @@ -1 +1 @@ -["mockFunctions", "mockInstance", "fakeTimers"] +["mockFunctions", "mockInstance", "fakeTimers", "utilities"] diff --git a/website/docs/zh/api/rstest/mockFunctions.mdx b/website/docs/zh/api/rstest/mockFunctions.mdx index 1119d220..90bb5e57 100644 --- a/website/docs/zh/api/rstest/mockFunctions.mdx +++ b/website/docs/zh/api/rstest/mockFunctions.mdx @@ -4,7 +4,7 @@ title: Mock functions # Mock functions -Rstest 提供了一些工具方法帮助你进行函数的模拟(mock)。 +Rstest 基于 [tinyspy](https://github.com/tinylibs/tinyspy) 提供了一些工具方法帮助你进行函数的模拟(mock)。 ## rstest.fn diff --git a/website/docs/zh/api/rstest/utilities.mdx b/website/docs/zh/api/rstest/utilities.mdx new file mode 100644 index 00000000..af6c8226 --- /dev/null +++ b/website/docs/zh/api/rstest/utilities.mdx @@ -0,0 +1,78 @@ +# Utilities + +一些实用的工具函数。 + +## rstest.stubEnv + +**类型:** `(name: string, value: string | undefined) => Rstest` + +临时设置 `process.env` 中的环境变量为指定值。适用于测试依赖环境变量的代码。 + +- 如果 `value` 为 `undefined`,该变量会从 `process.env` 中移除。 +- 可多次调用以模拟多个环境变量。 +- 使用 `rstest.unstubAllEnvs()` 可恢复所有通过此方法更改的环境变量。 + +**示例:** + +```ts +rstest.stubEnv('NODE_ENV', 'test'); +expect(process.env.NODE_ENV).toBe('test'); + +rstest.stubEnv('MY_VAR', undefined); +expect(process.env.MY_VAR).toBeUndefined(); +``` + +## rstest.unstubAllEnvs + +**类型:** `() => Rstest` + +恢复所有通过 `rstest.stubEnv` 更改的环境变量到原始值。 + +- 测试后调用此方法以清理环境变量。 +- 如果配置项 `unstubEnvs` 启用,则每个测试前会自动调用。 + +**示例:** + +```ts +rstest.stubEnv('NODE_ENV', 'test'); +// ... 执行相关代码 +rstest.unstubAllEnvs(); +expect(process.env.NODE_ENV).not.toBe('test'); +``` + +## rstest.stubGlobal + +**类型:** `(name: string | number | symbol, value: unknown) => Rstest` + +临时设置全局变量为指定值。适用于模拟全局对象或函数。 + +- 可多次调用以模拟多个全局变量。 +- 使用 `rstest.unstubAllGlobals()` 可恢复所有通过此方法更改的全局变量。 + +**示例:** + +```ts +rstest.stubGlobal('myGlobal', 123); +expect(globalThis.myGlobal).toBe(123); + +rstest.stubGlobal(Symbol.for('foo'), 'bar'); +expect(globalThis[Symbol.for('foo')]).toBe('bar'); +``` + +## rstest.unstubAllGlobals + +**类型:** `() => Rstest` + +恢复所有通过 `rstest.stubGlobal` 更改的全局变量到原始值。 + +- 测试后调用此方法以清理全局变量。 +- 如果配置项 `unstubGlobals` 启用,则每个测试前会自动调用。 + +**示例:** + +```ts +rstest.stubGlobal('myGlobal', 123); +// ... 执行相关代码 +rstest.unstubAllGlobals(); +expect(globalThis.myGlobal).toBeUndefined(); +``` diff --git a/website/docs/zh/api/test-api/_meta.json b/website/docs/zh/api/test-api/_meta.json index 9442282a..71a6e491 100644 --- a/website/docs/zh/api/test-api/_meta.json +++ b/website/docs/zh/api/test-api/_meta.json @@ -1 +1 @@ -["describe", "test", "hooks"] +["test", "describe", "hooks"] diff --git a/website/docs/zh/config/test/_meta.json b/website/docs/zh/config/test/_meta.json index bdc17b68..4a8a504d 100644 --- a/website/docs/zh/config/test/_meta.json +++ b/website/docs/zh/config/test/_meta.json @@ -17,5 +17,7 @@ "update", "clearMocks", "resetMocks", - "restoreMocks" + "restoreMocks", + "unstubEnvs", + "unstubGlobals" ] diff --git a/website/docs/zh/config/test/unstubEnvs.mdx b/website/docs/zh/config/test/unstubEnvs.mdx new file mode 100644 index 00000000..300bdc44 --- /dev/null +++ b/website/docs/zh/config/test/unstubEnvs.mdx @@ -0,0 +1,10 @@ +--- +overviewHeaders: [] +--- + +# unstubEnvs + +- **类型:** `boolean` +- **默认值:** `false` + +每次测试时恢复之前使用 [rstest.stubEnv](/api/rstest/utilities#rsteststubenv) 更改的所有 `process.env` 值。 diff --git a/website/docs/zh/config/test/unstubGlobals.mdx b/website/docs/zh/config/test/unstubGlobals.mdx new file mode 100644 index 00000000..6be6f26b --- /dev/null +++ b/website/docs/zh/config/test/unstubGlobals.mdx @@ -0,0 +1,10 @@ +--- +overviewHeaders: [] +--- + +# unstubGlobals + +- **类型:** `boolean` +- **默认值:** `false` + +每次测试时恢复之前使用 [rstest.stubGlobal](/api/rstest/utilities#rsteststubglobal) 更改的所有全局变量。 From 18ea5069d3de8cc02696fb4a747d8c239b69d324 Mon Sep 17 00:00:00 2001 From: Wei Date: Wed, 2 Jul 2025 15:18:05 +0800 Subject: [PATCH 07/10] fix(mock): resetModules should not clear mock (#349) --- packages/core/src/core/plugins/mockRuntime.ts | 17 ++++++++++++++--- pnpm-lock.yaml | 3 +++ tests/mock/tests/defaultInterop.test.ts | 13 +++++++++++++ tests/mock/tests/resetModules.test.ts | 9 +++++++++ tests/package.json | 3 ++- tests/rstest.config.ts | 5 +++++ 6 files changed, 46 insertions(+), 4 deletions(-) create mode 100644 tests/mock/tests/defaultInterop.test.ts diff --git a/packages/core/src/core/plugins/mockRuntime.ts b/packages/core/src/core/plugins/mockRuntime.ts index fcdc7cbd..4a2fdd83 100644 --- a/packages/core/src/core/plugins/mockRuntime.ts +++ b/packages/core/src/core/plugins/mockRuntime.ts @@ -36,7 +36,13 @@ __webpack_require__.rstest_original_modules = {}; // TODO: Remove "reset_modules" in next Rspack version. __webpack_require__.rstest_reset_modules = __webpack_require__.reset_modules = () => { - __webpack_module_cache__ = {}; + const mockedIds = Object.keys(__webpack_require__.rstest_original_modules) + Object.keys(__webpack_module_cache__).forEach(id => { + // Do not reset mocks registry. + if (!mockedIds.includes(id)) { + delete __webpack_module_cache__[id]; + } + }); } // TODO: Remove "unmock" in next Rspack version. @@ -54,15 +60,20 @@ __webpack_require__.rstest_require_actual = __webpack_require__.rstest_import_ac // TODO: Remove "set_mock" in next Rspack version. __webpack_require__.rstest_set_mock = __webpack_require__.set_mock = (id, modFactory) => { + let requiredModule = undefined try { - __webpack_require__.rstest_original_modules[id] = __webpack_require__(id); + requiredModule = __webpack_require__(id); } catch { // TODO: non-resolved module + } finally { + __webpack_require__.rstest_original_modules[id] = requiredModule; } if (typeof modFactory === 'string' || typeof modFactory === 'number') { __webpack_module_cache__[id] = { exports: __webpack_require__(modFactory) }; } else if (typeof modFactory === 'function') { - __webpack_module_cache__[id] = { exports: modFactory() }; + let exports = modFactory(); + __webpack_require__.r(exports); + __webpack_module_cache__[id] = { exports, id, loaded: true }; } }; `; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 90265240..f9763cd3 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -231,6 +231,9 @@ importers: pathe: specifier: ^2.0.3 version: 2.0.3 + react: + specifier: ^19.1.0 + version: 19.1.0 redux: specifier: ^5.0.1 version: 5.0.1 diff --git a/tests/mock/tests/defaultInterop.test.ts b/tests/mock/tests/defaultInterop.test.ts new file mode 100644 index 00000000..de0e6b63 --- /dev/null +++ b/tests/mock/tests/defaultInterop.test.ts @@ -0,0 +1,13 @@ +import { expect, rs, test } from '@rstest/core'; +// @ts-expect-error: "react" has been mocked. +import increment from 'react'; + +rs.mock('react', () => { + return { + default: (num: number) => num + 42, + }; +}); + +test('interop default export', async () => { + expect(increment(1)).toBe(43); +}); diff --git a/tests/mock/tests/resetModules.test.ts b/tests/mock/tests/resetModules.test.ts index 686d7394..0863266e 100644 --- a/tests/mock/tests/resetModules.test.ts +++ b/tests/mock/tests/resetModules.test.ts @@ -1,10 +1,19 @@ import { expect, rs, test } from '@rstest/core'; +rs.doMock('../src/increment', () => ({ + increment: (num: number) => num + 10, +})); + test('reset modules works', async () => { + const { increment: incrementWith10 } = await import('../src/increment'); + expect(incrementWith10(1)).toBe(11); const mod1 = await import('../src/b'); rs.resetModules(); const mod2 = await import('../src/b'); const mod3 = await import('../src/b'); expect(mod1).not.toBe(mod2); expect(mod2).toBe(mod3); + // resetModules should not reset mocks registry. + const { increment: incrementStillWith10 } = await import('../src/increment'); + expect(incrementStillWith10(1)).toBe(11); }); diff --git a/tests/package.json b/tests/package.json index e66cc93d..b223f887 100644 --- a/tests/package.json +++ b/tests/package.json @@ -14,11 +14,12 @@ "@types/jest-image-snapshot": "^6.4.0", "axios": "^1.10.0", "jest-image-snapshot": "^6.5.1", + "memfs": "^4.17.2", "pathe": "^2.0.3", + "react": "^19.1.0", "redux": "^5.0.1", "strip-ansi": "^7.1.0", "tinyexec": "^1.0.1", - "memfs": "^4.17.2", "typescript": "^5.8.3" } } diff --git a/tests/rstest.config.ts b/tests/rstest.config.ts index 879614ad..a99c4320 100644 --- a/tests/rstest.config.ts +++ b/tests/rstest.config.ts @@ -4,6 +4,11 @@ export default defineConfig({ setupFiles: ['../scripts/rstest.setup.ts'], testTimeout: process.env.CI ? 10_000 : 5_000, slowTestThreshold: 2_000, + output: { + externals: { + react: 'commonjs react', + }, + }, exclude: [ '**/node_modules/**', '**/dist/**', From 590534b325977a5db22148b57cd8e0cc76dea37d Mon Sep 17 00:00:00 2001 From: 9aoy Date: Wed, 2 Jul 2025 15:21:55 +0800 Subject: [PATCH 08/10] docs: add more test configs (#350) --- README.md | 4 ++++ website/docs/en/config/test/_meta.json | 6 +++++- .../docs/en/config/test/maxConcurrency.mdx | 8 ++++++++ .../docs/en/config/test/slowTestThreshold.mdx | 6 ++++++ .../docs/en/config/test/testNamePattern.mdx | 20 +++++++++++++++++++ website/docs/en/config/test/testTimeout.mdx | 6 ++++++ website/docs/zh/config/test/_meta.json | 6 +++++- .../docs/zh/config/test/maxConcurrency.mdx | 8 ++++++++ .../docs/zh/config/test/slowTestThreshold.mdx | 6 ++++++ .../docs/zh/config/test/testNamePattern.mdx | 20 +++++++++++++++++++ website/docs/zh/config/test/testTimeout.mdx | 6 ++++++ 11 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 website/docs/en/config/test/maxConcurrency.mdx create mode 100644 website/docs/en/config/test/slowTestThreshold.mdx create mode 100644 website/docs/en/config/test/testNamePattern.mdx create mode 100644 website/docs/en/config/test/testTimeout.mdx create mode 100644 website/docs/zh/config/test/maxConcurrency.mdx create mode 100644 website/docs/zh/config/test/slowTestThreshold.mdx create mode 100644 website/docs/zh/config/test/testNamePattern.mdx create mode 100644 website/docs/zh/config/test/testTimeout.mdx diff --git a/README.md b/README.md index f6da5d25..c756f68f 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ Rstest is a testing framework powered by Rspack. It delivers comprehensive, firs Rstest offers full Jest-compatible APIs while providing native, out-of-the-box support for TypeScript, ESM, and more, ensuring a modern and efficient testing experience. +## 📖 Documentation + +See [Documentation](https://rstest.rs). + ## 🔥 Roadmap Rstest is currently under active development and we plan to release the first stable version in late 2025. diff --git a/website/docs/en/config/test/_meta.json b/website/docs/en/config/test/_meta.json index 4a8a504d..26e095bc 100644 --- a/website/docs/en/config/test/_meta.json +++ b/website/docs/en/config/test/_meta.json @@ -5,6 +5,8 @@ "globals", "setupFiles", "testEnvironment", + "testNamePattern", + "testTimeout", "retry", "root", "name", @@ -19,5 +21,7 @@ "resetMocks", "restoreMocks", "unstubEnvs", - "unstubGlobals" + "unstubGlobals", + "maxConcurrency", + "slowTestThreshold" ] diff --git a/website/docs/en/config/test/maxConcurrency.mdx b/website/docs/en/config/test/maxConcurrency.mdx new file mode 100644 index 00000000..4df56838 --- /dev/null +++ b/website/docs/en/config/test/maxConcurrency.mdx @@ -0,0 +1,8 @@ +# maxConcurrency + +- **Type:** `number` +- **Default:** `5` + +A number limiting the number of test cases that are allowed to run at the same time when using `test.concurrent` or wrapped in `describe.concurrent`. + +Any test above this limit will be queued and executed once a slot is released. diff --git a/website/docs/en/config/test/slowTestThreshold.mdx b/website/docs/en/config/test/slowTestThreshold.mdx new file mode 100644 index 00000000..f9c181d3 --- /dev/null +++ b/website/docs/en/config/test/slowTestThreshold.mdx @@ -0,0 +1,6 @@ +# slowTestThreshold + +- **Type:** `number` +- **Default:** `300` + +The number of milliseconds after which a test or suite is considered slow and reported as such in the results. diff --git a/website/docs/en/config/test/testNamePattern.mdx b/website/docs/en/config/test/testNamePattern.mdx new file mode 100644 index 00000000..5ffe5598 --- /dev/null +++ b/website/docs/en/config/test/testNamePattern.mdx @@ -0,0 +1,20 @@ +# testNamePattern + +- **Type:** `string | RegExp` +- **Default:** `undefined` + +Run only tests with a name that matches the regex / string. + +If you set `testNamePattern` to `bar`, tests not containing the word `bar` in the test name will be skipped. + +```ts +// skipped +test('test foo', () => { + expect(true).toBe(true); +}); + +// run +test('test bar', () => { + expect(true).toBe(true); +}); +``` diff --git a/website/docs/en/config/test/testTimeout.mdx b/website/docs/en/config/test/testTimeout.mdx new file mode 100644 index 00000000..b6bc5c2a --- /dev/null +++ b/website/docs/en/config/test/testTimeout.mdx @@ -0,0 +1,6 @@ +# testTimeout + +- **Type:** `number` +- **Default:** `5_000` + +Default timeout of a test in milliseconds. `0` will disable the timeout. diff --git a/website/docs/zh/config/test/_meta.json b/website/docs/zh/config/test/_meta.json index 4a8a504d..26e095bc 100644 --- a/website/docs/zh/config/test/_meta.json +++ b/website/docs/zh/config/test/_meta.json @@ -5,6 +5,8 @@ "globals", "setupFiles", "testEnvironment", + "testNamePattern", + "testTimeout", "retry", "root", "name", @@ -19,5 +21,7 @@ "resetMocks", "restoreMocks", "unstubEnvs", - "unstubGlobals" + "unstubGlobals", + "maxConcurrency", + "slowTestThreshold" ] diff --git a/website/docs/zh/config/test/maxConcurrency.mdx b/website/docs/zh/config/test/maxConcurrency.mdx new file mode 100644 index 00000000..85768bb5 --- /dev/null +++ b/website/docs/zh/config/test/maxConcurrency.mdx @@ -0,0 +1,8 @@ +# maxConcurrency + +- **类型:** `number` +- **默认值:** `5` + +使用 `test.concurrent` 或 `describe.concurrent` 时允许同时运行的最大测试用例数量。 + +超过此限制数量的测试将在前序测试完成后排队运行。 diff --git a/website/docs/zh/config/test/slowTestThreshold.mdx b/website/docs/zh/config/test/slowTestThreshold.mdx new file mode 100644 index 00000000..7b787bd6 --- /dev/null +++ b/website/docs/zh/config/test/slowTestThreshold.mdx @@ -0,0 +1,6 @@ +# slowTestThreshold + +- **类型:** `number` +- **默认值:** `300` + +测试运行时间超过指定毫秒数时,被认为是缓慢的并在报告结果中显示。 diff --git a/website/docs/zh/config/test/testNamePattern.mdx b/website/docs/zh/config/test/testNamePattern.mdx new file mode 100644 index 00000000..aa1a29d9 --- /dev/null +++ b/website/docs/zh/config/test/testNamePattern.mdx @@ -0,0 +1,20 @@ +# testNamePattern + +- **类型:** `string | RegExp` +- **默认值:** `undefined` + +仅运行测试名称中匹配正则表达式或字符串的测试。 + +如果将 `testNamePattern` 设置为 `bar`,则测试名称中不包含 `bar` 的测试将被跳过。 + +```ts +// skipped +test('test foo', () => { + expect(true).toBe(true); +}); + +// run +test('test bar', () => { + expect(true).toBe(true); +}); +``` diff --git a/website/docs/zh/config/test/testTimeout.mdx b/website/docs/zh/config/test/testTimeout.mdx new file mode 100644 index 00000000..ca8e445a --- /dev/null +++ b/website/docs/zh/config/test/testTimeout.mdx @@ -0,0 +1,6 @@ +# testTimeout + +- **类型:** `number` +- **默认值:** `5_000` + +测试的默认超时时间(以毫秒为单位)。设置为 `0` 禁用超时。 From f5d361ac87ec4e5053021171514545bec44cddbe Mon Sep 17 00:00:00 2001 From: Wei Date: Wed, 2 Jul 2025 18:58:22 +0800 Subject: [PATCH 09/10] fix: enable --experimental-detect-module by default for node20 (#351) --- packages/core/src/core/rsbuild.ts | 5 +---- packages/core/src/pool/index.ts | 7 +++++-- packages/core/src/utils/helper.ts | 25 +++++++++++++++++++++---- 3 files changed, 27 insertions(+), 10 deletions(-) diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index 0625d626..554dbcb1 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -213,10 +213,7 @@ export const prepareRsbuild = async ( config.resolve.extensions ??= []; config.resolve.extensions.push('.cjs'); - if ( - testEnvironment === 'node' && - (getNodeVersion()[0] || 0) < 20 - ) { + if (testEnvironment === 'node' && getNodeVersion().major < 20) { // skip `module` field in Node.js 18 and below. // ESM module resolved by module field is not always a native ESM module config.resolve.mainFields = config.resolve.mainFields?.filter( diff --git a/packages/core/src/pool/index.ts b/packages/core/src/pool/index.ts index c7d00320..4c4a8cdb 100644 --- a/packages/core/src/pool/index.ts +++ b/packages/core/src/pool/index.ts @@ -10,7 +10,7 @@ import type { TestResult, UserConsoleLog, } from '../types'; -import { serializableConfig } from '../utils'; +import { needFlagExperimentalDetectModule, serializableConfig } from '../utils'; import { createForksPool } from './forks'; const getNumCpus = (): number => { @@ -106,7 +106,10 @@ export const createPool = async ({ '--experimental-vm-modules', '--experimental-import-meta-resolve', '--no-warnings', - ], + needFlagExperimentalDetectModule() + ? '--experimental-detect-module' + : undefined, + ].filter(Boolean) as string[], env: { NODE_ENV: 'test', // enable diff color by default diff --git a/packages/core/src/utils/helper.ts b/packages/core/src/utils/helper.ts index 8e39f26b..84d98a13 100644 --- a/packages/core/src/utils/helper.ts +++ b/packages/core/src/utils/helper.ts @@ -137,10 +137,27 @@ export const undoSerializableConfig = ( }; }; -export const getNodeVersion = (): number[] => { - return typeof process.versions?.node === 'string' - ? process.versions.node.split('.').map(Number) - : [0, 0, 0]; +export const getNodeVersion = (): { + major: number; + minor: number; + patch: number; +} => { + if (typeof process.versions?.node === 'string') { + const [major = 0, minor = 0, patch = 0] = process.versions.node + .split('.') + .map(Number); + return { major, minor, patch }; + } + return { major: 0, minor: 0, patch: 0 }; +}; + +export const needFlagExperimentalDetectModule = (): boolean => { + const { major, minor } = getNodeVersion(); + // `--experimental-detect-module` is introduced in Node.js 20.10.0. + if (major === 20 && minor >= 10) return true; + // `--experimental-detect-module` is enabled by default since Node.js 22.7.0. + if (major === 22 && minor < 7) return true; + return false; }; // Ported from https://github.com/webpack/webpack/blob/21b28a82f7a6ec677752e1c8fb722a830a2adf69/lib/node/NodeTargetPlugin.js. From f9288bcf150a526fc8b38343a67d823f87a22ff1 Mon Sep 17 00:00:00 2001 From: 9aoy Date: Wed, 2 Jul 2025 19:24:43 +0800 Subject: [PATCH 10/10] perf: add pre-filter for css (#352) --- biome.json | 2 +- packages/core/rslib.config.ts | 15 +++++ .../core/src/core/plugins/css-filter/index.ts | 67 +++++++++++++++++++ .../src/core/plugins/css-filter/loader.ts | 63 +++++++++++++++++ packages/core/src/core/rsbuild.ts | 2 + .../core/__snapshots__/rsbuild.test.ts.snap | 26 +++++++ tests/jsdom/css.test.ts | 21 ++++++ tests/jsdom/fixtures/rsbuild.config.ts | 5 ++ tests/jsdom/fixtures/src/App.css | 6 -- tests/jsdom/fixtures/src/App.module.css | 5 ++ tests/jsdom/fixtures/src/App.tsx | 5 +- tests/jsdom/fixtures/test/App.test.tsx | 1 - tests/jsdom/fixtures/test/css.test.tsx | 23 +++++++ 13 files changed, 232 insertions(+), 9 deletions(-) create mode 100644 packages/core/src/core/plugins/css-filter/index.ts create mode 100644 packages/core/src/core/plugins/css-filter/loader.ts create mode 100644 tests/jsdom/css.test.ts create mode 100644 tests/jsdom/fixtures/src/App.module.css create mode 100644 tests/jsdom/fixtures/test/css.test.tsx diff --git a/biome.json b/biome.json index 303388ce..2a86e132 100644 --- a/biome.json +++ b/biome.json @@ -9,7 +9,7 @@ }, "files": { "ignoreUnknown": true, - "includes": ["**", "!**/*.vue", "!**/dist/**"] + "includes": ["**", "!**/*.vue", "!**/dist/**", "!**/dist-types/**"] }, "formatter": { "indentStyle": "space" diff --git a/packages/core/rslib.config.ts b/packages/core/rslib.config.ts index ca031714..08ea4190 100644 --- a/packages/core/rslib.config.ts +++ b/packages/core/rslib.config.ts @@ -53,6 +53,21 @@ export default defineConfig({ }, }, }, + { + id: 'esm_loaders', + format: 'esm', + syntax: 'es2021', + source: { + entry: { + cssFilterLoader: './src/core/plugins/css-filter/loader.ts', + }, + }, + output: { + filename: { + js: '[name].mjs', + }, + }, + }, ], tools: { rspack: { diff --git a/packages/core/src/core/plugins/css-filter/index.ts b/packages/core/src/core/plugins/css-filter/index.ts new file mode 100644 index 00000000..3537565e --- /dev/null +++ b/packages/core/src/core/plugins/css-filter/index.ts @@ -0,0 +1,67 @@ +/** + * reference: + * https://github.com/rspack-contrib/rsbuild-plugin-typed-css-modules/blob/main/src/loader.ts + * https://github.com/web-infra-dev/rsbuild/blob/a0939d8994589819cc8ddd8982a69a0743a3227a/packages/core/src/loader/ignoreCssLoader.ts + */ +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import type { CSSLoaderOptions, RsbuildPlugin } from '@rsbuild/core'; + +export const PLUGIN_CSS_FILTER = 'rstest:css-filter'; +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +/** + * When CSS does not need to be emitted, pre-set the CSS content to empty (except for CSS modules) to reduce time costs in less-loader / sass-loader / css-loader. + */ +export const pluginCSSFilter = (): RsbuildPlugin => ({ + name: PLUGIN_CSS_FILTER, + + setup(api) { + api.modifyBundlerChain({ + order: 'post', + handler: async (chain, { target, CHAIN_ID, environment }) => { + const emitCss = environment.config.output.emitCss ?? target === 'web'; + if (!emitCss) { + const ruleIds = [ + CHAIN_ID.RULE.CSS, + CHAIN_ID.RULE.SASS, + CHAIN_ID.RULE.LESS, + CHAIN_ID.RULE.STYLUS, + ]; + + for (const ruleId of ruleIds) { + if (!chain.module.rules.has(ruleId)) { + continue; + } + + const rule = chain.module.rule(ruleId); + + if (!rule.uses.has(CHAIN_ID.USE.CSS)) { + continue; + } + + const cssLoaderOptions: CSSLoaderOptions = rule + .use(CHAIN_ID.USE.CSS) + .get('options'); + + if ( + !cssLoaderOptions.modules || + (typeof cssLoaderOptions.modules === 'object' && + cssLoaderOptions.modules.auto === false) + ) { + continue; + } + + rule + .use('rstest-css-pre-filter') + .loader(path.join(__dirname, 'cssFilterLoader.mjs')) + .options({ + modules: cssLoaderOptions.modules, + }) + .after(ruleId); + } + } + }, + }); + }, +}); diff --git a/packages/core/src/core/plugins/css-filter/loader.ts b/packages/core/src/core/plugins/css-filter/loader.ts new file mode 100644 index 00000000..d17ba2cf --- /dev/null +++ b/packages/core/src/core/plugins/css-filter/loader.ts @@ -0,0 +1,63 @@ +import type { CSSModules, Rspack } from '@rsbuild/core'; + +type CssLoaderModules = + | boolean + | string + | Required>; + +const CSS_MODULES_REGEX = /\.module\.\w+$/i; + +const isCSSModules = ({ + resourcePath, + resourceQuery, + resourceFragment, + modules, +}: { + resourcePath: string; + resourceQuery: string; + resourceFragment: string; + modules: CssLoaderModules; +}): boolean => { + if (typeof modules === 'boolean') { + return modules; + } + + // Same as the `mode` option + // https://github.com/webpack-contrib/css-loader?tab=readme-ov-file#mode + if (typeof modules === 'string') { + // CSS Modules will be disabled if mode is 'global' + return modules !== 'global'; + } + + const { auto } = modules; + + if (typeof auto === 'boolean') { + return auto && CSS_MODULES_REGEX.test(resourcePath); + } + if (auto instanceof RegExp) { + return auto.test(resourcePath); + } + if (typeof auto === 'function') { + return auto(resourcePath, resourceQuery, resourceFragment); + } + return true; +}; + +export default function ( + this: Rspack.LoaderContext<{ + mode: string; + modules: CssLoaderModules; + }>, + content: string, +): string { + const { resourcePath, resourceQuery, resourceFragment } = this; + const { modules = true } = this.getOptions() || {}; + + if ( + isCSSModules({ resourcePath, resourceQuery, resourceFragment, modules }) + ) { + return content; + } + + return ''; +} diff --git a/packages/core/src/core/rsbuild.ts b/packages/core/src/core/rsbuild.ts index 554dbcb1..41d5b24f 100644 --- a/packages/core/src/core/rsbuild.ts +++ b/packages/core/src/core/rsbuild.ts @@ -17,6 +17,7 @@ import { NODE_BUILTINS, TEMP_RSTEST_OUTPUT_DIR, } from '../utils'; +import { pluginCSSFilter } from './plugins/css-filter'; import { pluginEntryWatch } from './plugins/entry'; import { pluginIgnoreResolveError } from './plugins/ignoreResolveError'; import { pluginMockRuntime } from './plugins/mockRuntime'; @@ -235,6 +236,7 @@ export const prepareRsbuild = async ( plugins: [ pluginIgnoreResolveError, pluginMockRuntime, + pluginCSSFilter(), pluginEntryWatch({ globTestSourceEntries, setupFiles, diff --git a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap index 0a4f8986..ce16030e 100644 --- a/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap +++ b/packages/core/tests/core/__snapshots__/rsbuild.test.ts.snap @@ -77,6 +77,19 @@ exports[`prepareRsbuild > should generate rspack config correctly (jsdom) 1`] = "sourceMap": false, }, }, + { + "loader": "/packages/core/src/core/plugins/css-filter/cssFilterLoader.mjs", + "options": { + "modules": { + "auto": true, + "exportGlobals": false, + "exportLocalsConvention": "camelCase", + "exportOnlyLocals": true, + "localIdentName": "[path][name]__[local]-[hash:base64:6]", + "namedExport": false, + }, + }, + }, ], }, { @@ -543,6 +556,19 @@ exports[`prepareRsbuild > should generate rspack config correctly (node) 1`] = ` "sourceMap": false, }, }, + { + "loader": "/packages/core/src/core/plugins/css-filter/cssFilterLoader.mjs", + "options": { + "modules": { + "auto": true, + "exportGlobals": false, + "exportLocalsConvention": "camelCase", + "exportOnlyLocals": true, + "localIdentName": "[path][name]__[local]-[hash:base64:6]", + "namedExport": false, + }, + }, + }, ], }, { diff --git a/tests/jsdom/css.test.ts b/tests/jsdom/css.test.ts new file mode 100644 index 00000000..64ed4b58 --- /dev/null +++ b/tests/jsdom/css.test.ts @@ -0,0 +1,21 @@ +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { it } from '@rstest/core'; +import { runRstestCli } from '../scripts'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); + +it('should run css test correctly', async () => { + const { expectExecSuccess } = await runRstestCli({ + command: 'rstest', + args: ['run', 'test/css'], + options: { + nodeOptions: { + cwd: join(__dirname, 'fixtures'), + }, + }, + }); + + await expectExecSuccess(); +}); diff --git a/tests/jsdom/fixtures/rsbuild.config.ts b/tests/jsdom/fixtures/rsbuild.config.ts index c9962d33..010d491c 100644 --- a/tests/jsdom/fixtures/rsbuild.config.ts +++ b/tests/jsdom/fixtures/rsbuild.config.ts @@ -3,4 +3,9 @@ import { pluginReact } from '@rsbuild/plugin-react'; export default defineConfig({ plugins: [pluginReact()], + output: { + cssModules: { + localIdentName: '[name]_[local]', + }, + }, }); diff --git a/tests/jsdom/fixtures/src/App.css b/tests/jsdom/fixtures/src/App.css index 164c0a6a..6ec65a9b 100644 --- a/tests/jsdom/fixtures/src/App.css +++ b/tests/jsdom/fixtures/src/App.css @@ -18,9 +18,3 @@ body { font-size: 3.6rem; font-weight: 700; } - -.content p { - font-size: 1.2rem; - font-weight: 400; - opacity: 0.5; -} diff --git a/tests/jsdom/fixtures/src/App.module.css b/tests/jsdom/fixtures/src/App.module.css new file mode 100644 index 00000000..a225ceb6 --- /dev/null +++ b/tests/jsdom/fixtures/src/App.module.css @@ -0,0 +1,5 @@ +.content-p { + font-size: 1.2rem; + font-weight: 400; + opacity: 0.5; +} diff --git a/tests/jsdom/fixtures/src/App.tsx b/tests/jsdom/fixtures/src/App.tsx index 0ccfafe6..359a7edc 100644 --- a/tests/jsdom/fixtures/src/App.tsx +++ b/tests/jsdom/fixtures/src/App.tsx @@ -1,4 +1,5 @@ import './App.css'; +import style from './App.module.css'; const App = () => { return ( @@ -14,7 +15,9 @@ const App = () => { > Rsbuild with React -

Start building amazing things with Rsbuild.

+

+ Start building amazing things with Rsbuild. +

); }; diff --git a/tests/jsdom/fixtures/test/App.test.tsx b/tests/jsdom/fixtures/test/App.test.tsx index bdca666f..d70b1bef 100644 --- a/tests/jsdom/fixtures/test/App.test.tsx +++ b/tests/jsdom/fixtures/test/App.test.tsx @@ -9,7 +9,6 @@ test('should render App correctly', async () => { expect(element.tagName).toBe('H1'); - expect(element.style.fontSize).toBe('16px'); expect(element.constructor).toBe(document.defaultView?.HTMLHeadingElement); }); diff --git a/tests/jsdom/fixtures/test/css.test.tsx b/tests/jsdom/fixtures/test/css.test.tsx new file mode 100644 index 00000000..dd4764f0 --- /dev/null +++ b/tests/jsdom/fixtures/test/css.test.tsx @@ -0,0 +1,23 @@ +import { expect, test } from '@rstest/core'; +import { render, screen } from '@testing-library/react'; +import App from '../src/App'; + +test('should render App correctly', async () => { + render(); + + const element = screen.getByText('Rsbuild with React'); + + expect(element.tagName).toBe('H1'); + + expect(element.style.fontSize).toBe('16px'); + + const elementP = screen.getByText( + 'Start building amazing things with Rsbuild.', + ); + + expect(elementP.tagName).toBe('P'); + + expect(elementP.className).toBe('App-module_content-p'); + + expect(elementP.style.fontSize).toBe(''); +}); pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy